From 0ee500af3ed896054d041e5e820b815a6810cb0b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 00:33:00 +1000 Subject: [PATCH 01/45] Basic dependency resolution code --- .gitignore | 1 + Cargo.lock | 311 ++++++++++++++++++++++++++++++++++ Cargo.toml | 10 ++ rustfmt.toml | 1 + src/lib.rs | 3 + src/main.rs | 43 +++++ src/reporting/builders.rs | 245 +++++++++++++++++++++++++++ src/reporting/calculator.rs | 322 ++++++++++++++++++++++++++++++++++++ src/reporting/mod.rs | 161 ++++++++++++++++++ src/reporting/steps.rs | 198 ++++++++++++++++++++++ src/transaction.rs | 25 +++ src/util.rs | 24 +++ 12 files changed, 1344 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rustfmt.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/reporting/builders.rs create mode 100644 src/reporting/calculator.rs create mode 100644 src/reporting/mod.rs create mode 100644 src/reporting/steps.rs create mode 100644 src/transaction.rs create mode 100644 src/util.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf1c400 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "downcast-rs" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libdrcr" +version = "0.1.0" +dependencies = [ + "chrono", + "downcast-rs", + "solvent", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "solvent" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c925807 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "libdrcr" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.41" +downcast-rs = "2.0.1" +#dyn-clone = "1.0.19" +solvent = "0.8.3" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..24e0b08 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod reporting; +pub mod transaction; +pub mod util; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c728461 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,43 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use chrono::NaiveDate; +use libdrcr::reporting::{ + builders::register_dynamic_builders, + calculator::solve_for, + steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, + ReportingContext, ReportingStep, +}; + +fn main() { + let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + register_lookup_fns(&mut context); + register_dynamic_builders(&mut context); + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + Box::new(AllTransactionsExceptRetainedEarnings { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + ]; + + println!("{:?}", solve_for(targets, context)); +} diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs new file mode 100644 index 0000000..3987725 --- /dev/null +++ b/src/reporting/builders.rs @@ -0,0 +1,245 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use chrono::NaiveDate; + +use super::{ + calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, + ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, + ReportingStepDynamicBuilder, ReportingStepId, +}; + +pub fn register_dynamic_builders(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: BalancesAtToBalancesBetween::can_build, + build: BalancesAtToBalancesBetween::build, + }); + + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesBetween", + can_build: UpdateBalancesBetween::can_build, + build: UpdateBalancesBetween::build, + }); +} + +#[derive(Debug)] +pub struct BalancesAtToBalancesBetween { + step_name: &'static str, + date_start: NaiveDate, + date_end: NaiveDate, +} + +impl BalancesAtToBalancesBetween { + // Implements BalancesAt, BalancesAt -> BalancesBetween + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for BalancesAt, BalancesAt -> BalancesBetween + if kind == ReportingProductKind::BalancesBetween { + match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::BalancesAt, + args: vec![args[1].clone()], + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + return true; + } + HasStepOrCanBuild::None => {} + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Vec, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(BalancesAtToBalancesBetween { + step_name: name, + date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for BalancesAtToBalancesBetween { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesBetween], + args: vec![ + self.date_start.format("%Y-%m-%d").to_string(), + self.date_end.format("%Y-%m-%d").to_string(), + ], + } + } + + fn init_graph( + &self, + _steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: vec![self.date_start.format("%Y-%m-%d").to_string()], + }, + ); + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: vec![self.date_end.format("%Y-%m-%d").to_string()], + }, + ); + } +} + +#[derive(Debug)] +pub struct UpdateBalancesBetween { + step_name: &'static str, + date_start: NaiveDate, + date_end: NaiveDate, +} + +impl UpdateBalancesBetween { + // Implements (BalancesBetween -> Transactions) -> BalancesBetween + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + _args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesBetween + if kind == ReportingProductKind::BalancesBetween { + // Initially no need to check args + if let Some(step) = steps.iter().find(|s| { + s.id().name == name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) { + // Check for BalancesBetween -> Transactions + let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind + == ReportingProductKind::BalancesBetween + { + return true; + } + } + + // Check lookup or builder - with args + /*match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::Transactions, + args: args.clone(), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(step) => unreachable!(), + HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) + | HasStepOrCanBuild::None => {} + }*/ + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Vec, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(UpdateBalancesBetween { + step_name: name, + date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for UpdateBalancesBetween { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesBetween], + args: vec![ + self.date_start.format("%Y-%m-%d").to_string(), + self.date_end.format("%Y-%m-%d").to_string(), + ], + } + } + + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + // Add a dependency on the Transactions result + // Look up that step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args.clone(), + }, + ); + } +} diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs new file mode 100644 index 0000000..e8fb151 --- /dev/null +++ b/src/reporting/calculator.rs @@ -0,0 +1,322 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use super::{ + ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, + ReportingStepDynamicBuilder, ReportingStepId, ReportingStepLookupFn, +}; + +#[derive(Debug)] +pub struct ReportingGraphDependencies { + vec: Vec, +} + +impl ReportingGraphDependencies { + pub fn vec(&self) -> &Vec { + &self.vec + } + + pub fn add_dependency(&mut self, step: ReportingStepId, dependency: ReportingProductId) { + if !self + .vec + .iter() + .any(|d| d.step == step && d.dependency == dependency) + { + self.vec.push(Dependency { step, dependency }); + } + } + + pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { + for kind in target.product_kinds { + match kind { + ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { + self.add_dependency( + target.clone(), + ReportingProductId { + name: dependency.name, + kind: *kind, + args: target.args.clone(), + }, + ); + } + ReportingProductKind::BalancesAt => todo!(), + ReportingProductKind::Generic => todo!(), + } + } + } + + pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { + return self.vec.iter().filter(|d| d.step == *step).collect(); + } +} + +#[derive(Debug)] +pub struct Dependency { + pub step: ReportingStepId, + pub dependency: ReportingProductId, +} + +#[derive(Debug)] +pub enum ReportingCalculationError { + UnknownStep { message: String }, + NoStepForProduct { message: String }, + CircularDependencies, +} + +pub enum HasStepOrCanBuild<'a, 'b> { + HasStep(&'a Box), + CanLookup(ReportingStepLookupFn), + CanBuild(&'b ReportingStepDynamicBuilder), + None, +} + +pub fn has_step_or_can_build<'a, 'b>( + product: &ReportingProductId, + steps: &'a Vec>, + dependencies: &ReportingGraphDependencies, + context: &'b ReportingContext, +) -> HasStepOrCanBuild<'a, 'b> { + if let Some(step) = steps.iter().find(|s| { + s.id().name == product.name + && s.id().args == product.args + && s.id().product_kinds.contains(&product.kind) + }) { + return HasStepOrCanBuild::HasStep(step); + } + + // Try lookup function + if let Some(lookup_key) = context + .step_lookup_fn + .keys() + .find(|(name, kinds)| *name == product.name && kinds.contains(&product.kind)) + { + return HasStepOrCanBuild::CanLookup(*context.step_lookup_fn.get(lookup_key).unwrap()); + } + + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + product.name, + product.kind, + product.args.clone(), + steps, + dependencies, + context, + ) { + return HasStepOrCanBuild::CanBuild(builder); + } + } + + return HasStepOrCanBuild::None; +} + +fn would_be_ready_to_execute( + step: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + previous_steps: &Vec, +) -> bool { + //println!( + // "- would_be_ready_to_execute: {}, {:?}", + // step.id(), + // previous_steps + //); + + // Check whether the step would be ready to execute, if the previous steps have already completed + 'check_each_dependency: for dependency in dependencies.vec.iter() { + if dependency.step == step.id() { + //println!("-- {}", dependency.dependency); + + // Check if the dependency has been produced by a previous step + for previous_step in previous_steps { + if steps[*previous_step].id().name == dependency.dependency.name + && steps[*previous_step].id().args == dependency.dependency.args + && steps[*previous_step] + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + continue 'check_each_dependency; + } + } + + // Dependency is not met + return false; + } + } + true +} + +pub fn solve_for( + targets: Vec>, + context: ReportingContext, +) -> Result>, ReportingCalculationError> { + let mut steps: Vec> = Vec::new(); + let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; + + // Initialise targets + for target in targets { + steps.push(target); + let target = steps.last().unwrap(); + target.as_ref().init_graph(&steps, &mut dependencies); + } + + // Call after_init_graph on targets + for step in steps.iter() { + step.as_ref().after_init_graph(&steps, &mut dependencies); + } + + // Process dependencies + loop { + let mut new_steps = Vec::new(); + + for dependency in dependencies.vec.iter() { + if !steps.iter().any(|s| s.id() == dependency.step) { + // FIXME: Call the lookup function + todo!(); + } + if !steps.iter().any(|s| { + s.id().name == dependency.dependency.name + && s.id().args == dependency.dependency.args + && s.id().product_kinds.contains(&dependency.dependency.kind) + }) { + // Try lookup function + if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { + *name == dependency.dependency.name + && kinds.contains(&dependency.dependency.kind) + }) { + let lookup_fn = context.step_lookup_fn.get(lookup_key).unwrap(); + let new_step = lookup_fn(dependency.dependency.args.clone()); + + // Check new step meets the dependency + if new_step.id().name != dependency.dependency.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + } + if new_step.id().args != dependency.dependency.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + } + if !new_step + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + } + + new_steps.push(new_step); + } else { + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + dependency.dependency.name, + dependency.dependency.kind, + dependency.dependency.args.clone(), + &steps, + &dependencies, + &context, + ) { + new_steps.push((builder.build)( + dependency.dependency.name, + dependency.dependency.kind, + dependency.dependency.args.clone(), + &steps, + &dependencies, + &context, + )); + break; + } + } + } + } + } + + if new_steps.len() == 0 { + break; + } + + // Initialise new steps + let mut new_step_indexes = Vec::new(); + for new_step in new_steps { + new_step_indexes.push(steps.len()); + steps.push(new_step); + let new_step = steps.last().unwrap(); + new_step.as_ref().init_graph(&steps, &mut dependencies); + } + + // Call after_init_graph on new steps + for new_step_index in new_step_indexes { + steps[new_step_index].after_init_graph(&steps, &mut dependencies); + } + } + + // Check all dependencies satisfied + for dependency in dependencies.vec.iter() { + if !steps.iter().any(|s| s.id() == dependency.step) { + return Err(ReportingCalculationError::UnknownStep { + message: format!( + "No implementation for step {} which {} is a dependency of", + dependency.step, dependency.dependency + ), + }); + } + if !steps.iter().any(|s| { + s.id().name == dependency.dependency.name + && s.id().args == dependency.dependency.args + && s.id().product_kinds.contains(&dependency.dependency.kind) + }) { + return Err(ReportingCalculationError::NoStepForProduct { + message: format!( + "No step builds product {} wanted by {}", + dependency.dependency, dependency.step + ), + }); + } + } + + // Sort + let mut sorted_step_indexes = Vec::new(); + let mut steps_remaining = steps.iter().enumerate().collect::>(); + + 'loop_until_all_sorted: while !steps_remaining.is_empty() { + for (cur_index, (orig_index, step)) in steps_remaining.iter().enumerate() { + if would_be_ready_to_execute(step, &steps, &dependencies, &sorted_step_indexes) { + sorted_step_indexes.push(*orig_index); + steps_remaining.remove(cur_index); + continue 'loop_until_all_sorted; + } + } + + // No steps to execute - must be circular dependency + return Err(ReportingCalculationError::CircularDependencies); + } + + let mut sort_mapping = vec![0_usize; sorted_step_indexes.len()]; + for i in 0..sorted_step_indexes.len() { + sort_mapping[sorted_step_indexes[i]] = i; + } + + // TODO: This can be done in place + let mut sorted_steps = steps.into_iter().zip(sort_mapping).collect::>(); + sorted_steps.sort_unstable_by_key(|(_s, order)| *order); + let sorted_steps = sorted_steps + .into_iter() + .map(|(s, _idx)| s) + .collect::>(); + + Ok(sorted_steps) +} diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs new file mode 100644 index 0000000..8bd4012 --- /dev/null +++ b/src/reporting/mod.rs @@ -0,0 +1,161 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use std::fmt::Debug; +use std::{collections::HashMap, fmt::Display}; + +use calculator::{ReportingGraphDependencies}; +use chrono::NaiveDate; +use downcast_rs::Downcast; + +pub mod builders; +pub mod calculator; +pub mod steps; + +pub struct ReportingContext { + _eofy_date: NaiveDate, + step_lookup_fn: HashMap<(&'static str, &'static [ReportingProductKind]), ReportingStepLookupFn>, + step_dynamic_builders: Vec, +} + +impl ReportingContext { + pub fn new(eofy_date: NaiveDate) -> Self { + Self { + _eofy_date: eofy_date, + step_lookup_fn: HashMap::new(), + step_dynamic_builders: Vec::new(), + } + } + + fn register_lookup_fn( + &mut self, + name: &'static str, + product_kinds: &'static [ReportingProductKind], + builder: ReportingStepLookupFn, + ) { + self.step_lookup_fn.insert((name, product_kinds), builder); + } + + fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { + if !self + .step_dynamic_builders + .iter() + .any(|b| b.name == builder.name) + { + self.step_dynamic_builders.push(builder); + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct ReportingProductId { + name: &'static str, + kind: ReportingProductKind, + args: Vec, +} + +impl Display for ReportingProductId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}.{:?}{:?}", self.name, self.kind, self.args)) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ReportingProductKind { + Transactions, + BalancesAt, + BalancesBetween, + Generic, +} + +//enum ReportingProduct { +// Transactions(Transactions), +// BalancesAt(BalancesAt), +// BalancesBetween(BalancesBetween), +// Generic(Box), +//} + +//struct Transactions {} +//struct BalancesAt {} +//struct BalancesBetween {} + +//trait GenericReportingProduct {} + +//type ReportingProducts = HashMap; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReportingStepId { + pub name: &'static str, + pub product_kinds: &'static [ReportingProductKind], + pub args: Vec, +} + +impl Display for ReportingStepId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}{:?}{:?}", + self.name, self.product_kinds, self.args + )) + } +} + +pub trait ReportingStep: Debug + Downcast { + // Info + fn id(&self) -> ReportingStepId; + + // Methods + fn init_graph( + &self, + _steps: &Vec>, + _dependencies: &mut ReportingGraphDependencies, + ) { + } + fn after_init_graph( + &self, + _steps: &Vec>, + _dependencies: &mut ReportingGraphDependencies, + ) { + } + //fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) { + // todo!(); + //} +} + +downcast_rs::impl_downcast!(ReportingStep); + +pub type ReportingStepLookupFn = fn(args: Vec) -> Box; + +pub struct ReportingStepDynamicBuilder { + name: &'static str, + can_build: fn( + name: &'static str, + kind: ReportingProductKind, + args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool, + build: fn( + name: &'static str, + kind: ReportingProductKind, + args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> Box, +} diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs new file mode 100644 index 0000000..575b3e7 --- /dev/null +++ b/src/reporting/steps.rs @@ -0,0 +1,198 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use chrono::NaiveDate; + +use crate::util::sofy_from_eofy; + +use super::{ + calculator::ReportingGraphDependencies, ReportingContext, ReportingProductId, + ReportingProductKind, ReportingStep, ReportingStepId, +}; + +pub fn register_lookup_fns(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + AllTransactionsExceptRetainedEarnings::from_args, + ); + + context.register_lookup_fn( + "CalculateIncomeTax", + &[ReportingProductKind::Transactions], + CalculateIncomeTax::from_args, + ); + + context.register_lookup_fn( + "CombineOrdinaryTransactions", + &[ReportingProductKind::BalancesAt], + CombineOrdinaryTransactions::from_args, + ); + + context.register_lookup_fn( + "DBBalances", + &[ReportingProductKind::BalancesAt], + DBBalances::from_args, + ); +} + +#[derive(Debug)] +pub struct AllTransactionsExceptRetainedEarnings { + pub date_start: NaiveDate, + pub date_end: NaiveDate, +} + +impl AllTransactionsExceptRetainedEarnings { + fn from_args(args: Vec) -> Box { + Box::new(AllTransactionsExceptRetainedEarnings { + date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for AllTransactionsExceptRetainedEarnings { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "AllTransactionsExceptRetainedEarnings", + product_kinds: &[ReportingProductKind::BalancesBetween], + args: vec![ + self.date_start.format("%Y-%m-%d").to_string(), + self.date_end.format("%Y-%m-%d").to_string(), + ], + } + } +} + +#[derive(Debug)] +pub struct CalculateIncomeTax { + pub date_eofy: NaiveDate, +} + +impl CalculateIncomeTax { + fn from_args(args: Vec) -> Box { + Box::new(CalculateIncomeTax { + date_eofy: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for CalculateIncomeTax { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "CalculateIncomeTax", + product_kinds: &[ReportingProductKind::Transactions], + args: vec![self.date_eofy.format("%Y-%m-%d").to_string()], + } + } + + fn init_graph( + &self, + _steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesBetween, + args: vec![ + sofy_from_eofy(self.date_eofy) + .format("%Y-%m-%d") + .to_string(), + self.date_eofy.format("%Y-%m-%d").to_string(), + ], + }, + ); + } + + fn after_init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + for other in steps { + if let Some(other) = other.downcast_ref::() { + if other.date_start <= self.date_eofy && other.date_end >= self.date_eofy { + dependencies.add_target_dependency(other.id(), self.id()); + } + } + } + } +} + +#[derive(Debug)] +pub struct CombineOrdinaryTransactions { + pub date: NaiveDate, +} + +impl CombineOrdinaryTransactions { + fn from_args(args: Vec) -> Box { + Box::new(CombineOrdinaryTransactions { + date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for CombineOrdinaryTransactions { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "CombineOrdinaryTransactions", + product_kinds: &[ReportingProductKind::BalancesAt], + args: vec![self.date.format("%Y-%m-%d").to_string()], + } + } + + fn init_graph( + &self, + _steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: "DBBalances", + kind: ReportingProductKind::BalancesAt, + args: vec![self.date.format("%Y-%m-%d").to_string()], + }, + ); + } +} + +#[derive(Debug)] +pub struct DBBalances { + pub date: NaiveDate, +} + +impl DBBalances { + fn from_args(args: Vec) -> Box { + Box::new(DBBalances { + date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for DBBalances { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "DBBalances", + product_kinds: &[ReportingProductKind::BalancesAt], + args: vec![self.date.format("%Y-%m-%d").to_string()], + } + } +} diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000..a17ebbc --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,25 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use chrono::NaiveDateTime; + +pub struct Transaction { + pub id: Option, + pub dt: NaiveDateTime, + pub description: String, +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..c8129c4 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,24 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use chrono::{Datelike, NaiveDate}; + +pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { + // Return the start date of the financial year, given the end date of the financial year + return date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap(); +} From de890aeade875938e4449976ec852a4226748c9a Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 16:39:18 +1000 Subject: [PATCH 02/45] Refactor representation of ReportingStep args --- Cargo.lock | 14 ++++++++ Cargo.toml | 3 +- src/main.rs | 21 +++++++++--- src/reporting/builders.rs | 47 ++++++++++++--------------- src/reporting/calculator.rs | 4 +-- src/reporting/mod.rs | 64 ++++++++++++++++++++++++++++++++----- src/reporting/steps.rs | 63 +++++++++++++++++------------------- 7 files changed, 140 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf1c400..fee6985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "dyn-eq" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -116,6 +128,8 @@ version = "0.1.0" dependencies = [ "chrono", "downcast-rs", + "dyn-clone", + "dyn-eq", "solvent", ] diff --git a/Cargo.toml b/Cargo.toml index c925807..b4bac49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" [dependencies] chrono = "0.4.41" downcast-rs = "2.0.1" -#dyn-clone = "1.0.19" +dyn-clone = "1.0.19" +dyn-eq = "0.1.3" solvent = "0.8.3" diff --git a/src/main.rs b/src/main.rs index c728461..5870164 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use libdrcr::reporting::{ builders::register_dynamic_builders, calculator::solve_for, steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, - ReportingContext, ReportingStep, + DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingStep, }; fn main() { @@ -31,13 +31,24 @@ fn main() { let targets: Vec> = vec![ Box::new(CalculateIncomeTax { - date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + args: DateEofyArgs { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, }), Box::new(AllTransactionsExceptRetainedEarnings { - date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), - date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + args: DateStartDateEndArgs { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, }), ]; - println!("{:?}", solve_for(targets, context)); + match solve_for(targets, context) { + Ok(steps) => { + for step in steps { + println!("- {}", step.id()); + } + } + Err(err) => panic!("Error: {:?}", err), + } } diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 3987725..6d91e61 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,12 +16,10 @@ along with this program. If not, see . */ -use chrono::NaiveDate; - use super::{ calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepId, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, + ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; pub fn register_dynamic_builders(context: &mut ReportingContext) { @@ -41,8 +39,7 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { #[derive(Debug)] pub struct BalancesAtToBalancesBetween { step_name: &'static str, - date_start: NaiveDate, - date_end: NaiveDate, + args: DateStartDateEndArgs, } impl BalancesAtToBalancesBetween { @@ -51,7 +48,7 @@ impl BalancesAtToBalancesBetween { fn can_build( name: &'static str, kind: ReportingProductKind, - args: Vec, + args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, @@ -62,7 +59,7 @@ impl BalancesAtToBalancesBetween { &ReportingProductId { name, kind: ReportingProductKind::BalancesAt, - args: vec![args[1].clone()], + args: args.clone(), }, steps, dependencies, @@ -82,15 +79,14 @@ impl BalancesAtToBalancesBetween { fn build( name: &'static str, _kind: ReportingProductKind, - args: Vec, + args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(BalancesAtToBalancesBetween { step_name: name, - date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), - date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -100,10 +96,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesBetween], - args: vec![ - self.date_start.format("%Y-%m-%d").to_string(), - self.date_end.format("%Y-%m-%d").to_string(), - ], + args: Box::new(self.args.clone()), } } @@ -112,12 +105,15 @@ impl ReportingStep for BalancesAtToBalancesBetween { _steps: &Vec>, dependencies: &mut ReportingGraphDependencies, ) { + // BalancesAtToBalancesBetween depends on BalancesAt at both time points dependencies.add_dependency( self.id(), ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, - args: vec![self.date_start.format("%Y-%m-%d").to_string()], + args: Box::new(DateArgs { + date: self.args.date_start.clone(), + }), }, ); dependencies.add_dependency( @@ -125,7 +121,9 @@ impl ReportingStep for BalancesAtToBalancesBetween { ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, - args: vec![self.date_end.format("%Y-%m-%d").to_string()], + args: Box::new(DateArgs { + date: self.args.date_end.clone(), + }), }, ); } @@ -134,8 +132,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, - date_start: NaiveDate, - date_end: NaiveDate, + args: DateStartDateEndArgs, } impl UpdateBalancesBetween { @@ -144,7 +141,7 @@ impl UpdateBalancesBetween { fn can_build( name: &'static str, kind: ReportingProductKind, - _args: Vec, + _args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, _context: &ReportingContext, @@ -191,15 +188,14 @@ impl UpdateBalancesBetween { fn build( name: &'static str, _kind: ReportingProductKind, - args: Vec, + args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(UpdateBalancesBetween { step_name: name, - date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), - date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -209,10 +205,7 @@ impl ReportingStep for UpdateBalancesBetween { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesBetween], - args: vec![ - self.date_start.format("%Y-%m-%d").to_string(), - self.date_end.format("%Y-%m-%d").to_string(), - ], + args: Box::new(self.args.clone()), } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index e8fb151..07a1ddc 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -113,7 +113,7 @@ pub fn has_step_or_can_build<'a, 'b>( if (builder.can_build)( product.name, product.kind, - product.args.clone(), + &product.args, steps, dependencies, context, @@ -225,7 +225,7 @@ pub fn solve_for( if (builder.can_build)( dependency.dependency.name, dependency.dependency.kind, - dependency.dependency.args.clone(), + &dependency.dependency.args, &steps, &dependencies, &context, diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 8bd4012..5e27a52 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -19,9 +19,11 @@ use std::fmt::Debug; use std::{collections::HashMap, fmt::Display}; -use calculator::{ReportingGraphDependencies}; +use calculator::ReportingGraphDependencies; use chrono::NaiveDate; use downcast_rs::Downcast; +use dyn_clone::DynClone; +use dyn_eq::DynEq; pub mod builders; pub mod calculator; @@ -66,12 +68,12 @@ impl ReportingContext { pub struct ReportingProductId { name: &'static str, kind: ReportingProductKind, - args: Vec, + args: Box, } impl Display for ReportingProductId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}.{:?}{:?}", self.name, self.kind, self.args)) + f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args)) } } @@ -102,13 +104,13 @@ pub enum ReportingProductKind { pub struct ReportingStepId { pub name: &'static str, pub product_kinds: &'static [ReportingProductKind], - pub args: Vec, + pub args: Box, } impl Display for ReportingStepId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( - "{}{:?}{:?}", + "{}{:?}({})", self.name, self.product_kinds, self.args )) } @@ -138,14 +140,60 @@ pub trait ReportingStep: Debug + Downcast { downcast_rs::impl_downcast!(ReportingStep); -pub type ReportingStepLookupFn = fn(args: Vec) -> Box; +pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq {} + +downcast_rs::impl_downcast!(ReportingStepArgs); +dyn_clone::clone_trait_object!(ReportingStepArgs); +dyn_eq::eq_trait_object!(ReportingStepArgs); + +pub type ReportingStepLookupFn = fn(args: Box) -> Box; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateArgs { + pub date: NaiveDate, +} + +impl ReportingStepArgs for DateArgs {} + +impl Display for DateArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.date)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateEofyArgs { + pub date_eofy: NaiveDate, +} + +impl ReportingStepArgs for DateEofyArgs {} + +impl Display for DateEofyArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.date_eofy)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateStartDateEndArgs { + pub date_start: NaiveDate, + pub date_end: NaiveDate, +} + +impl ReportingStepArgs for DateStartDateEndArgs {} + +impl Display for DateStartDateEndArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) + } +} pub struct ReportingStepDynamicBuilder { name: &'static str, can_build: fn( name: &'static str, kind: ReportingProductKind, - args: Vec, + args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, @@ -153,7 +201,7 @@ pub struct ReportingStepDynamicBuilder { build: fn( name: &'static str, kind: ReportingProductKind, - args: Vec, + args: Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 575b3e7..42a90b8 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,13 +16,12 @@ along with this program. If not, see . */ -use chrono::NaiveDate; - use crate::util::sofy_from_eofy; use super::{ - calculator::ReportingGraphDependencies, ReportingContext, ReportingProductId, - ReportingProductKind, ReportingStep, ReportingStepId, + calculator::ReportingGraphDependencies, DateArgs, DateEofyArgs, DateStartDateEndArgs, + ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, + ReportingStepId, }; pub fn register_lookup_fns(context: &mut ReportingContext) { @@ -53,15 +52,13 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub date_start: NaiveDate, - pub date_end: NaiveDate, + pub args: DateStartDateEndArgs, } impl AllTransactionsExceptRetainedEarnings { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { - date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), - date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -71,23 +68,20 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { ReportingStepId { name: "AllTransactionsExceptRetainedEarnings", product_kinds: &[ReportingProductKind::BalancesBetween], - args: vec![ - self.date_start.format("%Y-%m-%d").to_string(), - self.date_end.format("%Y-%m-%d").to_string(), - ], + args: Box::new(self.args.clone()), } } } #[derive(Debug)] pub struct CalculateIncomeTax { - pub date_eofy: NaiveDate, + pub args: DateEofyArgs, } impl CalculateIncomeTax { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { - date_eofy: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -97,7 +91,7 @@ impl ReportingStep for CalculateIncomeTax { ReportingStepId { name: "CalculateIncomeTax", product_kinds: &[ReportingProductKind::Transactions], - args: vec![self.date_eofy.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), } } @@ -106,17 +100,16 @@ impl ReportingStep for CalculateIncomeTax { _steps: &Vec>, dependencies: &mut ReportingGraphDependencies, ) { + // CalculateIncomeTax depends on CombineOrdinaryTransactions dependencies.add_dependency( self.id(), ReportingProductId { name: "CombineOrdinaryTransactions", kind: ReportingProductKind::BalancesBetween, - args: vec![ - sofy_from_eofy(self.date_eofy) - .format("%Y-%m-%d") - .to_string(), - self.date_eofy.format("%Y-%m-%d").to_string(), - ], + args: Box::new(DateStartDateEndArgs { + date_start: sofy_from_eofy(self.args.date_eofy), + date_end: self.args.date_eofy.clone(), + }), }, ); } @@ -128,7 +121,10 @@ impl ReportingStep for CalculateIncomeTax { ) { for other in steps { if let Some(other) = other.downcast_ref::() { - if other.date_start <= self.date_eofy && other.date_end >= self.date_eofy { + if other.args.date_start <= self.args.date_eofy + && other.args.date_end >= self.args.date_eofy + { + // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax dependencies.add_target_dependency(other.id(), self.id()); } } @@ -138,13 +134,13 @@ impl ReportingStep for CalculateIncomeTax { #[derive(Debug)] pub struct CombineOrdinaryTransactions { - pub date: NaiveDate, + pub args: DateArgs, } impl CombineOrdinaryTransactions { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { - date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -154,7 +150,7 @@ impl ReportingStep for CombineOrdinaryTransactions { ReportingStepId { name: "CombineOrdinaryTransactions", product_kinds: &[ReportingProductKind::BalancesAt], - args: vec![self.date.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), } } @@ -163,12 +159,13 @@ impl ReportingStep for CombineOrdinaryTransactions { _steps: &Vec>, dependencies: &mut ReportingGraphDependencies, ) { + // CombineOrdinaryTransactions depends on DBBalances dependencies.add_dependency( self.id(), ReportingProductId { name: "DBBalances", kind: ReportingProductKind::BalancesAt, - args: vec![self.date.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), }, ); } @@ -176,13 +173,13 @@ impl ReportingStep for CombineOrdinaryTransactions { #[derive(Debug)] pub struct DBBalances { - pub date: NaiveDate, + pub args: DateArgs, } impl DBBalances { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(DBBalances { - date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -192,7 +189,7 @@ impl ReportingStep for DBBalances { ReportingStepId { name: "DBBalances", product_kinds: &[ReportingProductKind::BalancesAt], - args: vec![self.date.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), } } } From 39617a54ac5e55c9c5ee56e3353e585ae1ebb9b4 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 17:11:20 +1000 Subject: [PATCH 03/45] Implement GenerateBalances dynamic builder --- src/reporting/builders.rs | 108 ++++++++++++++++++++++++++++++++---- src/reporting/calculator.rs | 17 ++++-- src/reporting/mod.rs | 20 +++++-- src/reporting/steps.rs | 98 +++++++++++++++++++++++--------- 4 files changed, 198 insertions(+), 45 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 6d91e61..af5e455 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -29,6 +29,12 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { build: BalancesAtToBalancesBetween::build, }); + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "GenerateBalances", + can_build: GenerateBalances::can_build, + build: GenerateBalances::build, + }); + context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesBetween", can_build: UpdateBalancesBetween::can_build, @@ -55,11 +61,15 @@ impl BalancesAtToBalancesBetween { ) -> bool { // Check for BalancesAt, BalancesAt -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { + let args = args.downcast_ref::().unwrap(); + match has_step_or_can_build( &ReportingProductId { name, kind: ReportingProductKind::BalancesAt, - args: args.clone(), + args: Box::new(DateArgs { + date: args.date_start.clone(), + }), }, steps, dependencies, @@ -100,14 +110,9 @@ impl ReportingStep for BalancesAtToBalancesBetween { } } - fn init_graph( - &self, - _steps: &Vec>, - dependencies: &mut ReportingGraphDependencies, - ) { + fn requires(&self) -> Vec { // BalancesAtToBalancesBetween depends on BalancesAt at both time points - dependencies.add_dependency( - self.id(), + vec![ ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -115,9 +120,6 @@ impl ReportingStep for BalancesAtToBalancesBetween { date: self.args.date_start.clone(), }), }, - ); - dependencies.add_dependency( - self.id(), ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -125,7 +127,89 @@ impl ReportingStep for BalancesAtToBalancesBetween { date: self.args.date_end.clone(), }), }, - ); + ] + } +} + +#[derive(Debug)] +pub struct GenerateBalances { + step_name: &'static str, + args: DateArgs, +} + +impl GenerateBalances { + // Implements (() -> Transactions) -> BalancesAt + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesAt + if kind == ReportingProductKind::BalancesAt { + match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::Transactions, + args: args.clone(), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(step) => { + // Check for () -> Transactions + if dependencies.dependencies_for_step(&step.id()).len() == 0 { + return true; + } + } + HasStepOrCanBuild::CanLookup(lookup_fn) => { + // Check for () -> Transactions + let step = lookup_fn(args.clone()); + if step.requires().len() == 0 { + return true; + } + } + HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Box, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(GenerateBalances { + step_name: name, + args: *args.downcast().unwrap(), + }) + } +} + +impl ReportingStep for GenerateBalances { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self) -> Vec { + // GenerateBalances depends on Transactions + vec![ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }] } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 07a1ddc..e5c2771 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -18,7 +18,7 @@ use super::{ ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepId, ReportingStepLookupFn, + ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, }; #[derive(Debug)] @@ -80,7 +80,7 @@ pub enum ReportingCalculationError { pub enum HasStepOrCanBuild<'a, 'b> { HasStep(&'a Box), - CanLookup(ReportingStepLookupFn), + CanLookup(ReportingStepFromArgsFn), CanBuild(&'b ReportingStepDynamicBuilder), None, } @@ -105,7 +105,10 @@ pub fn has_step_or_can_build<'a, 'b>( .keys() .find(|(name, kinds)| *name == product.name && kinds.contains(&product.kind)) { - return HasStepOrCanBuild::CanLookup(*context.step_lookup_fn.get(lookup_key).unwrap()); + let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); + if takes_args_fn(&product.args) { + return HasStepOrCanBuild::CanLookup(*from_args_fn); + } } // No explicit step for product - try builders @@ -173,6 +176,9 @@ pub fn solve_for( for target in targets { steps.push(target); let target = steps.last().unwrap(); + for dependency in target.requires() { + dependencies.add_dependency(target.id(), dependency); + } target.as_ref().init_graph(&steps, &mut dependencies); } @@ -200,7 +206,7 @@ pub fn solve_for( *name == dependency.dependency.name && kinds.contains(&dependency.dependency.kind) }) { - let lookup_fn = context.step_lookup_fn.get(lookup_key).unwrap(); + let (_, lookup_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); let new_step = lookup_fn(dependency.dependency.args.clone()); // Check new step meets the dependency @@ -255,6 +261,9 @@ pub fn solve_for( new_step_indexes.push(steps.len()); steps.push(new_step); let new_step = steps.last().unwrap(); + for dependency in new_step.requires() { + dependencies.add_dependency(new_step.id(), dependency); + } new_step.as_ref().init_graph(&steps, &mut dependencies); } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 5e27a52..dd0fe23 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -31,7 +31,10 @@ pub mod steps; pub struct ReportingContext { _eofy_date: NaiveDate, - step_lookup_fn: HashMap<(&'static str, &'static [ReportingProductKind]), ReportingStepLookupFn>, + step_lookup_fn: HashMap< + (&'static str, &'static [ReportingProductKind]), + (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), + >, step_dynamic_builders: Vec, } @@ -48,9 +51,11 @@ impl ReportingContext { &mut self, name: &'static str, product_kinds: &'static [ReportingProductKind], - builder: ReportingStepLookupFn, + takes_args_fn: ReportingStepTakesArgsFn, + from_args_fn: ReportingStepFromArgsFn, ) { - self.step_lookup_fn.insert((name, product_kinds), builder); + self.step_lookup_fn + .insert((name, product_kinds), (takes_args_fn, from_args_fn)); } fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { @@ -121,18 +126,24 @@ pub trait ReportingStep: Debug + Downcast { fn id(&self) -> ReportingStepId; // Methods + fn requires(&self) -> Vec { + vec![] + } + fn init_graph( &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, ) { } + fn after_init_graph( &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, ) { } + //fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) { // todo!(); //} @@ -146,7 +157,8 @@ downcast_rs::impl_downcast!(ReportingStepArgs); dyn_clone::clone_trait_object!(ReportingStepArgs); dyn_eq::eq_trait_object!(ReportingStepArgs); -pub type ReportingStepLookupFn = fn(args: Box) -> Box; +pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; +pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; #[derive(Clone, Debug, Eq, PartialEq)] pub struct DateArgs { diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 42a90b8..3c94461 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -28,26 +28,37 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { context.register_lookup_fn( "AllTransactionsExceptRetainedEarnings", &[ReportingProductKind::BalancesBetween], + AllTransactionsExceptRetainedEarnings::takes_args, AllTransactionsExceptRetainedEarnings::from_args, ); context.register_lookup_fn( "CalculateIncomeTax", &[ReportingProductKind::Transactions], + CalculateIncomeTax::takes_args, CalculateIncomeTax::from_args, ); context.register_lookup_fn( "CombineOrdinaryTransactions", &[ReportingProductKind::BalancesAt], + CombineOrdinaryTransactions::takes_args, CombineOrdinaryTransactions::from_args, ); context.register_lookup_fn( "DBBalances", &[ReportingProductKind::BalancesAt], + DBBalances::takes_args, DBBalances::from_args, ); + + context.register_lookup_fn( + "PostUnreconciledStatementLines", + &[ReportingProductKind::Transactions], + PostUnreconciledStatementLines::takes_args, + PostUnreconciledStatementLines::from_args, + ); } #[derive(Debug)] @@ -56,6 +67,10 @@ pub struct AllTransactionsExceptRetainedEarnings { } impl AllTransactionsExceptRetainedEarnings { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { args: *args.downcast().unwrap(), @@ -79,6 +94,10 @@ pub struct CalculateIncomeTax { } impl CalculateIncomeTax { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { args: *args.downcast().unwrap(), @@ -95,23 +114,16 @@ impl ReportingStep for CalculateIncomeTax { } } - fn init_graph( - &self, - _steps: &Vec>, - dependencies: &mut ReportingGraphDependencies, - ) { + fn requires(&self) -> Vec { // CalculateIncomeTax depends on CombineOrdinaryTransactions - dependencies.add_dependency( - self.id(), - ReportingProductId { - name: "CombineOrdinaryTransactions", - kind: ReportingProductKind::BalancesBetween, - args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(self.args.date_eofy), - date_end: self.args.date_eofy.clone(), - }), - }, - ); + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: sofy_from_eofy(self.args.date_eofy), + date_end: self.args.date_eofy.clone(), + }), + }] } fn after_init_graph( @@ -138,6 +150,10 @@ pub struct CombineOrdinaryTransactions { } impl CombineOrdinaryTransactions { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { args: *args.downcast().unwrap(), @@ -154,20 +170,21 @@ impl ReportingStep for CombineOrdinaryTransactions { } } - fn init_graph( - &self, - _steps: &Vec>, - dependencies: &mut ReportingGraphDependencies, - ) { - // CombineOrdinaryTransactions depends on DBBalances - dependencies.add_dependency( - self.id(), + fn requires(&self) -> Vec { + vec![ + // CombineOrdinaryTransactions depends on DBBalances ReportingProductId { name: "DBBalances", kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, - ); + // CombineOrdinaryTransactions depends on PostUnreconciledStatementLines + ReportingProductId { + name: "PostUnreconciledStatementLines", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + ] } } @@ -177,6 +194,10 @@ pub struct DBBalances { } impl DBBalances { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(DBBalances { args: *args.downcast().unwrap(), @@ -193,3 +214,30 @@ impl ReportingStep for DBBalances { } } } + +#[derive(Debug)] +pub struct PostUnreconciledStatementLines { + pub args: DateArgs, +} + +impl PostUnreconciledStatementLines { + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(PostUnreconciledStatementLines { + args: *args.downcast().unwrap(), + }) + } +} + +impl ReportingStep for PostUnreconciledStatementLines { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "PostUnreconciledStatementLines", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } +} From 61ed6f82d7426d0fc352a0727b49377373ca9313 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 17:16:25 +1000 Subject: [PATCH 04/45] Implement Display for ReportingStep --- src/main.rs | 2 +- src/reporting/builders.rs | 20 ++++++++++++++++++++ src/reporting/mod.rs | 2 +- src/reporting/steps.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5870164..30bd029 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ fn main() { match solve_for(targets, context) { Ok(steps) => { for step in steps { - println!("- {}", step.id()); + println!("- {}", step); } } Err(err) => panic!("Error: {:?}", err), diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index af5e455..7905298 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::fmt::Display; + use super::{ calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, @@ -101,6 +103,12 @@ impl BalancesAtToBalancesBetween { } } +impl Display for BalancesAtToBalancesBetween { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{BalancesAtToBalancesBetween}}", self.id())) + } +} + impl ReportingStep for BalancesAtToBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -194,6 +202,12 @@ impl GenerateBalances { } } +impl Display for GenerateBalances { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{GenerateBalances}}", self.id())) + } +} + impl ReportingStep for GenerateBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -284,6 +298,12 @@ impl UpdateBalancesBetween { } } +impl Display for UpdateBalancesBetween { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{UpdateBalancesBetween}}", self.id())) + } +} + impl ReportingStep for UpdateBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index dd0fe23..b0155bd 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -121,7 +121,7 @@ impl Display for ReportingStepId { } } -pub trait ReportingStep: Debug + Downcast { +pub trait ReportingStep: Debug + Display + Downcast { // Info fn id(&self) -> ReportingStepId; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 3c94461..c3f8f19 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::fmt::Display; + use crate::util::sofy_from_eofy; use super::{ @@ -78,6 +80,12 @@ impl AllTransactionsExceptRetainedEarnings { } } +impl Display for AllTransactionsExceptRetainedEarnings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for AllTransactionsExceptRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -105,6 +113,12 @@ impl CalculateIncomeTax { } } +impl Display for CalculateIncomeTax { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for CalculateIncomeTax { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -161,6 +175,12 @@ impl CombineOrdinaryTransactions { } } +impl Display for CombineOrdinaryTransactions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for CombineOrdinaryTransactions { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -205,6 +225,12 @@ impl DBBalances { } } +impl Display for DBBalances { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for DBBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -232,6 +258,12 @@ impl PostUnreconciledStatementLines { } } +impl Display for PostUnreconciledStatementLines { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for PostUnreconciledStatementLines { fn id(&self) -> ReportingStepId { ReportingStepId { From 5a1b54f78246eb9e09b7d63546934b8c153bc1d3 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 17:58:42 +1000 Subject: [PATCH 05/45] Implement UpdateBalancesAt --- src/main.rs | 42 +++++++++- src/reporting/builders.rs | 155 ++++++++++++++++++++++++++++++++++-- src/reporting/calculator.rs | 84 +++++++++---------- src/reporting/steps.rs | 149 +++++++++++++++++++++++++++++----- 4 files changed, 361 insertions(+), 69 deletions(-) diff --git a/src/main.rs b/src/main.rs index 30bd029..02ea9f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,12 @@ use chrono::NaiveDate; use libdrcr::reporting::{ builders::register_dynamic_builders, calculator::solve_for, - steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, - DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingStep, + steps::{ + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, + }, + DateArgs, DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, + ReportingStep, }; fn main() { @@ -36,13 +40,43 @@ fn main() { }, }), Box::new(AllTransactionsExceptRetainedEarnings { - args: DateStartDateEndArgs { + product_kinds: &[ReportingProductKind::BalancesBetween], + args: Box::new(DateStartDateEndArgs { date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, + }), }), ]; + println!("For income statement:"); + match solve_for(targets, context) { + Ok(steps) => { + for step in steps { + println!("- {}", step); + } + } + Err(err) => panic!("Error: {:?}", err), + } + + let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + register_lookup_fns(&mut context); + register_dynamic_builders(&mut context); + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax { + args: DateEofyArgs { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, + }), + Box::new(AllTransactionsIncludingRetainedEarnings { + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }), + ]; + + println!("For balance sheet:"); match solve_for(targets, context) { Ok(steps) => { for step in steps { diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 7905298..369df9c 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -25,12 +25,6 @@ use super::{ }; pub fn register_dynamic_builders(context: &mut ReportingContext) { - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "BalancesAtToBalancesBetween", - can_build: BalancesAtToBalancesBetween::can_build, - build: BalancesAtToBalancesBetween::build, - }); - context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", can_build: GenerateBalances::can_build, @@ -42,6 +36,19 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { can_build: UpdateBalancesBetween::can_build, build: UpdateBalancesBetween::build, }); + + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesAt", + can_build: UpdateBalancesAt::can_build, + build: UpdateBalancesAt::build, + }); + + // This is the least efficient way of generating BalancesBetween + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: BalancesAtToBalancesBetween::can_build, + build: BalancesAtToBalancesBetween::build, + }); } #[derive(Debug)] @@ -63,6 +70,10 @@ impl BalancesAtToBalancesBetween { ) -> bool { // Check for BalancesAt, BalancesAt -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { + if !args.is::() { + return false; + } + let args = args.downcast_ref::().unwrap(); match has_step_or_can_build( @@ -105,7 +116,10 @@ impl BalancesAtToBalancesBetween { impl Display for BalancesAtToBalancesBetween { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{} {{BalancesAtToBalancesBetween}}", self.id())) + f.write_fmt(format_args!( + "{} {{BalancesAtToBalancesBetween}}", + self.id() + )) } } @@ -227,6 +241,133 @@ impl ReportingStep for GenerateBalances { } } +#[derive(Debug)] +pub struct UpdateBalancesAt { + step_name: &'static str, + args: DateArgs, +} + +impl UpdateBalancesAt { + // Implements (BalancesAt -> Transactions) -> BalancesAt + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + _args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesAt + if kind == ReportingProductKind::BalancesAt { + // Initially no need to check args + if let Some(step) = steps.iter().find(|s| { + s.id().name == name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) { + // Check for BalancesAt -> Transactions + let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesAt + { + return true; + } + + // Check if BalancesBetween -> Transactions and BalancesAt is available + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind + == ReportingProductKind::BalancesBetween + { + let date_end = dependencies_for_step[0] + .dependency + .args + .downcast_ref::() + .unwrap() + .date_end; + + match has_step_or_can_build( + &ReportingProductId { + name: dependencies_for_step[0].dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: date_end }), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + return true; + } + HasStepOrCanBuild::None => {} + } + } + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Box, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(UpdateBalancesAt { + step_name: name, + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for UpdateBalancesAt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{UpdateBalancesAt}}", self.id())) + } +} + +impl ReportingStep for UpdateBalancesAt { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), + } + } + + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + // Add a dependency on the Transactions result + // Look up that step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args.clone(), + }, + ); + } +} + #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index e5c2771..2073a43 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -17,8 +17,8 @@ */ use super::{ - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, + ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder, + ReportingStepFromArgsFn, ReportingStepId, }; #[derive(Debug)] @@ -41,7 +41,7 @@ impl ReportingGraphDependencies { } } - pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { + /*pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { for kind in target.product_kinds { match kind { ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { @@ -58,7 +58,7 @@ impl ReportingGraphDependencies { ReportingProductKind::Generic => todo!(), } } - } + }*/ pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { return self.vec.iter().filter(|d| d.step == *step).collect(); @@ -206,46 +206,50 @@ pub fn solve_for( *name == dependency.dependency.name && kinds.contains(&dependency.dependency.kind) }) { - let (_, lookup_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); - let new_step = lookup_fn(dependency.dependency.args.clone()); + let (takes_args_fn, from_args_fn) = + context.step_lookup_fn.get(lookup_key).unwrap(); + if takes_args_fn(&dependency.dependency.args) { + let new_step = from_args_fn(dependency.dependency.args.clone()); - // Check new step meets the dependency - if new_step.id().name != dependency.dependency.name { - panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); - } - if new_step.id().args != dependency.dependency.args { - panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); - } - if !new_step - .id() - .product_kinds - .contains(&dependency.dependency.kind) - { - panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); - } + // Check new step meets the dependency + if new_step.id().name != dependency.dependency.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + } + if new_step.id().args != dependency.dependency.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + } + if !new_step + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + } - new_steps.push(new_step); - } else { - // No explicit step for product - try builders - for builder in context.step_dynamic_builders.iter() { - if (builder.can_build)( + new_steps.push(new_step); + continue; + } + } + + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + dependency.dependency.name, + dependency.dependency.kind, + &dependency.dependency.args, + &steps, + &dependencies, + &context, + ) { + new_steps.push((builder.build)( dependency.dependency.name, dependency.dependency.kind, - &dependency.dependency.args, + dependency.dependency.args.clone(), &steps, &dependencies, &context, - ) { - new_steps.push((builder.build)( - dependency.dependency.name, - dependency.dependency.kind, - dependency.dependency.args.clone(), - &steps, - &dependencies, - &context, - )); - break; - } + )); + break; } } } @@ -267,9 +271,9 @@ pub fn solve_for( new_step.as_ref().init_graph(&steps, &mut dependencies); } - // Call after_init_graph on new steps - for new_step_index in new_step_indexes { - steps[new_step_index].after_init_graph(&steps, &mut dependencies); + // Call after_init_graph on all steps + for step in steps.iter() { + step.as_ref().after_init_graph(&steps, &mut dependencies); } } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index c3f8f19..bf512ed 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -27,11 +27,49 @@ use super::{ }; pub fn register_lookup_fns(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesAt], + AllTransactionsExceptRetainedEarnings::takes_args, + |a| { + AllTransactionsExceptRetainedEarnings::from_args(&[ReportingProductKind::BalancesAt], a) + }, + ); + context.register_lookup_fn( "AllTransactionsExceptRetainedEarnings", &[ReportingProductKind::BalancesBetween], AllTransactionsExceptRetainedEarnings::takes_args, - AllTransactionsExceptRetainedEarnings::from_args, + |a| { + AllTransactionsExceptRetainedEarnings::from_args( + &[ReportingProductKind::BalancesBetween], + a, + ) + }, + ); + + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesAt], + AllTransactionsIncludingRetainedEarnings::takes_args, + |a| { + AllTransactionsIncludingRetainedEarnings::from_args( + &[ReportingProductKind::BalancesAt], + a, + ) + }, + ); + + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + AllTransactionsIncludingRetainedEarnings::takes_args, + |a| { + AllTransactionsIncludingRetainedEarnings::from_args( + &[ReportingProductKind::BalancesBetween], + a, + ) + }, ); context.register_lookup_fn( @@ -65,17 +103,22 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub args: DateStartDateEndArgs, + pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub args: Box, } impl AllTransactionsExceptRetainedEarnings { - fn takes_args(args: &Box) -> bool { - args.is::() + fn takes_args(_args: &Box) -> bool { + true } - - fn from_args(args: Box) -> Box { + + fn from_args( + product_kinds: &'static [ReportingProductKind], + args: Box, + ) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { - args: *args.downcast().unwrap(), + product_kinds, + args, }) } } @@ -90,12 +133,58 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { name: "AllTransactionsExceptRetainedEarnings", - product_kinds: &[ReportingProductKind::BalancesBetween], - args: Box::new(self.args.clone()), + product_kinds: self.product_kinds, + args: self.args.clone(), } } } +#[derive(Debug)] +pub struct AllTransactionsIncludingRetainedEarnings { + pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub args: Box, +} + +impl AllTransactionsIncludingRetainedEarnings { + fn takes_args(_args: &Box) -> bool { + true + } + + fn from_args( + product_kinds: &'static [ReportingProductKind], + args: Box, + ) -> Box { + Box::new(AllTransactionsIncludingRetainedEarnings { + product_kinds, + args, + }) + } +} + +impl Display for AllTransactionsIncludingRetainedEarnings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for AllTransactionsIncludingRetainedEarnings { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "AllTransactionsIncludingRetainedEarnings", + product_kinds: self.product_kinds, + args: self.args.clone(), + } + } + + fn requires(&self) -> Vec { + vec![ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: self.product_kinds[0], + args: self.args.clone(), + }] + } +} + #[derive(Debug)] pub struct CalculateIncomeTax { pub args: DateEofyArgs, @@ -105,7 +194,7 @@ impl CalculateIncomeTax { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { args: *args.downcast().unwrap(), @@ -147,11 +236,35 @@ impl ReportingStep for CalculateIncomeTax { ) { for other in steps { if let Some(other) = other.downcast_ref::() { - if other.args.date_start <= self.args.date_eofy - && other.args.date_end >= self.args.date_eofy - { - // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax - dependencies.add_target_dependency(other.id(), self.id()); + // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax + if other.args.is::() { + let other_args = other.args.downcast_ref::().unwrap(); + if other_args.date >= self.args.date_eofy { + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); + } + } else if other.args.is::() { + let other_args = other.args.downcast_ref::().unwrap(); + if other_args.date_start <= self.args.date_eofy + && other_args.date_end >= self.args.date_eofy + { + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); + } + } else { + unreachable!(); } } } @@ -167,7 +280,7 @@ impl CombineOrdinaryTransactions { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { args: *args.downcast().unwrap(), @@ -217,7 +330,7 @@ impl DBBalances { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(DBBalances { args: *args.downcast().unwrap(), @@ -250,7 +363,7 @@ impl PostUnreconciledStatementLines { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(PostUnreconciledStatementLines { args: *args.downcast().unwrap(), From 161acabb7d533427c54b2459616d555b3b5606d2 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:10:08 +1000 Subject: [PATCH 06/45] Refactor CalculateIncomeTax --- src/main.rs | 15 ++------ src/reporting/builders.rs | 8 +++-- src/reporting/calculator.rs | 12 +++---- src/reporting/mod.rs | 32 ++++++++--------- src/reporting/steps.rs | 72 +++++++++++++------------------------ 5 files changed, 54 insertions(+), 85 deletions(-) diff --git a/src/main.rs b/src/main.rs index 02ea9f7..a80fcae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,8 +24,7 @@ use libdrcr::reporting::{ register_lookup_fns, AllTransactionsExceptRetainedEarnings, AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, }, - DateArgs, DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, - ReportingStep, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, }; fn main() { @@ -34,11 +33,7 @@ fn main() { register_dynamic_builders(&mut context); let targets: Vec> = vec![ - Box::new(CalculateIncomeTax { - args: DateEofyArgs { - date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), + Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsExceptRetainedEarnings { product_kinds: &[ReportingProductKind::BalancesBetween], args: Box::new(DateStartDateEndArgs { @@ -63,11 +58,7 @@ fn main() { register_dynamic_builders(&mut context); let targets: Vec> = vec![ - Box::new(CalculateIncomeTax { - args: DateEofyArgs { - date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), + Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsIncludingRetainedEarnings { product_kinds: &[ReportingProductKind::BalancesAt], args: Box::new(DateArgs { diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 369df9c..1f89da1 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -132,7 +132,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { // BalancesAtToBalancesBetween depends on BalancesAt at both time points vec![ ReportingProductId { @@ -191,7 +191,7 @@ impl GenerateBalances { HasStepOrCanBuild::CanLookup(lookup_fn) => { // Check for () -> Transactions let step = lookup_fn(args.clone()); - if step.requires().len() == 0 { + if step.requires(context).len() == 0 { return true; } } @@ -231,7 +231,7 @@ impl ReportingStep for GenerateBalances { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { // GenerateBalances depends on Transactions vec![ReportingProductId { name: self.step_name, @@ -344,6 +344,7 @@ impl ReportingStep for UpdateBalancesAt { &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { // Add a dependency on the Transactions result // Look up that step, so we can extract the appropriate args @@ -458,6 +459,7 @@ impl ReportingStep for UpdateBalancesBetween { &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { // Add a dependency on the Transactions result // Look up that step, so we can extract the appropriate args diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 2073a43..dc7d11e 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -176,15 +176,15 @@ pub fn solve_for( for target in targets { steps.push(target); let target = steps.last().unwrap(); - for dependency in target.requires() { + for dependency in target.requires(&context) { dependencies.add_dependency(target.id(), dependency); } - target.as_ref().init_graph(&steps, &mut dependencies); + target.as_ref().init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on targets for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies); + step.as_ref().after_init_graph(&steps, &mut dependencies, &context); } // Process dependencies @@ -265,15 +265,15 @@ pub fn solve_for( new_step_indexes.push(steps.len()); steps.push(new_step); let new_step = steps.last().unwrap(); - for dependency in new_step.requires() { + for dependency in new_step.requires(&context) { dependencies.add_dependency(new_step.id(), dependency); } - new_step.as_ref().init_graph(&steps, &mut dependencies); + new_step.as_ref().init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on all steps for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies); + step.as_ref().after_init_graph(&steps, &mut dependencies, &context); } } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index b0155bd..5a1ae40 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -30,7 +30,7 @@ pub mod calculator; pub mod steps; pub struct ReportingContext { - _eofy_date: NaiveDate, + eofy_date: NaiveDate, step_lookup_fn: HashMap< (&'static str, &'static [ReportingProductKind]), (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), @@ -41,7 +41,7 @@ pub struct ReportingContext { impl ReportingContext { pub fn new(eofy_date: NaiveDate) -> Self { Self { - _eofy_date: eofy_date, + eofy_date: eofy_date, step_lookup_fn: HashMap::new(), step_dynamic_builders: Vec::new(), } @@ -126,7 +126,7 @@ pub trait ReportingStep: Debug + Display + Downcast { fn id(&self) -> ReportingStepId; // Methods - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { vec![] } @@ -134,6 +134,7 @@ pub trait ReportingStep: Debug + Display + Downcast { &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { } @@ -141,6 +142,7 @@ pub trait ReportingStep: Debug + Display + Downcast { &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { } @@ -160,6 +162,17 @@ dyn_eq::eq_trait_object!(ReportingStepArgs); pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VoidArgs {} + +impl ReportingStepArgs for VoidArgs {} + +impl Display for VoidArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("")) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct DateArgs { pub date: NaiveDate, @@ -173,19 +186,6 @@ impl Display for DateArgs { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DateEofyArgs { - pub date_eofy: NaiveDate, -} - -impl ReportingStepArgs for DateEofyArgs {} - -impl Display for DateEofyArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.date_eofy)) - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct DateStartDateEndArgs { pub date_start: NaiveDate, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index bf512ed..60b3c3e 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -21,9 +21,9 @@ use std::fmt::Display; use crate::util::sofy_from_eofy; use super::{ - calculator::ReportingGraphDependencies, DateArgs, DateEofyArgs, DateStartDateEndArgs, - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, - ReportingStepId, + calculator::ReportingGraphDependencies, DateArgs, DateStartDateEndArgs, ReportingContext, + ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepId, + VoidArgs, }; pub fn register_lookup_fns(context: &mut ReportingContext) { @@ -176,7 +176,7 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { vec![ReportingProductId { name: "AllTransactionsExceptRetainedEarnings", kind: self.product_kinds[0], @@ -186,19 +186,15 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { } #[derive(Debug)] -pub struct CalculateIncomeTax { - pub args: DateEofyArgs, -} +pub struct CalculateIncomeTax {} impl CalculateIncomeTax { - fn takes_args(args: &Box) -> bool { - args.is::() + fn takes_args(_args: &Box) -> bool { + true } - fn from_args(args: Box) -> Box { - Box::new(CalculateIncomeTax { - args: *args.downcast().unwrap(), - }) + fn from_args(_args: Box) -> Box { + Box::new(CalculateIncomeTax {}) } } @@ -213,18 +209,18 @@ impl ReportingStep for CalculateIncomeTax { ReportingStepId { name: "CalculateIncomeTax", product_kinds: &[ReportingProductKind::Transactions], - args: Box::new(self.args.clone()), + args: Box::new(VoidArgs {}), } } - fn requires(&self) -> Vec { + fn requires(&self, context: &ReportingContext) -> Vec { // CalculateIncomeTax depends on CombineOrdinaryTransactions vec![ReportingProductId { name: "CombineOrdinaryTransactions", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(self.args.date_eofy), - date_end: self.args.date_eofy.clone(), + date_start: sofy_from_eofy(context.eofy_date), + date_end: context.eofy_date.clone(), }), }] } @@ -233,39 +229,19 @@ impl ReportingStep for CalculateIncomeTax { &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { for other in steps { if let Some(other) = other.downcast_ref::() { - // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax - if other.args.is::() { - let other_args = other.args.downcast_ref::().unwrap(); - if other_args.date >= self.args.date_eofy { - dependencies.add_dependency( - other.id(), - ReportingProductId { - name: self.id().name, - kind: other.product_kinds[0], - args: other.id().args, - }, - ); - } - } else if other.args.is::() { - let other_args = other.args.downcast_ref::().unwrap(); - if other_args.date_start <= self.args.date_eofy - && other_args.date_end >= self.args.date_eofy - { - dependencies.add_dependency( - other.id(), - ReportingProductId { - name: self.id().name, - kind: other.product_kinds[0], - args: other.id().args, - }, - ); - } - } else { - unreachable!(); - } + // AllTransactionsExceptRetainedEarnings depends on CalculateIncomeTax + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); } } } @@ -303,7 +279,7 @@ impl ReportingStep for CombineOrdinaryTransactions { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { vec![ // CombineOrdinaryTransactions depends on DBBalances ReportingProductId { From 1e33074b4de811cb982999085b61210c5df7d8a4 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:20:19 +1000 Subject: [PATCH 07/45] Refactor AllTransactionsIncludingRetainedEarnings --- src/main.rs | 5 ++-- src/reporting/steps.rs | 57 ++++++++++++++++-------------------------- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/main.rs b/src/main.rs index a80fcae..b112c11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,10 +60,9 @@ fn main() { let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsIncludingRetainedEarnings { - product_kinds: &[ReportingProductKind::BalancesAt], - args: Box::new(DateArgs { + args: DateArgs { date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }), + }, }), ]; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 60b3c3e..e98c162 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -52,24 +52,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { "AllTransactionsIncludingRetainedEarnings", &[ReportingProductKind::BalancesAt], AllTransactionsIncludingRetainedEarnings::takes_args, - |a| { - AllTransactionsIncludingRetainedEarnings::from_args( - &[ReportingProductKind::BalancesAt], - a, - ) - }, - ); - - context.register_lookup_fn( - "AllTransactionsIncludingRetainedEarnings", - &[ReportingProductKind::BalancesBetween], - AllTransactionsIncludingRetainedEarnings::takes_args, - |a| { - AllTransactionsIncludingRetainedEarnings::from_args( - &[ReportingProductKind::BalancesBetween], - a, - ) - }, + AllTransactionsIncludingRetainedEarnings::from_args, ); context.register_lookup_fn( @@ -141,22 +124,17 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { #[derive(Debug)] pub struct AllTransactionsIncludingRetainedEarnings { - pub product_kinds: &'static [ReportingProductKind], // Must have single member - pub args: Box, + pub args: DateArgs, } impl AllTransactionsIncludingRetainedEarnings { - fn takes_args(_args: &Box) -> bool { - true + fn takes_args(args: &Box) -> bool { + args.is::() } - fn from_args( - product_kinds: &'static [ReportingProductKind], - args: Box, - ) -> Box { + fn from_args(args: Box) -> Box { Box::new(AllTransactionsIncludingRetainedEarnings { - product_kinds, - args, + args: *args.downcast().unwrap(), }) } } @@ -171,17 +149,26 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { name: "AllTransactionsIncludingRetainedEarnings", - product_kinds: self.product_kinds, - args: self.args.clone(), + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), } } fn requires(&self, _context: &ReportingContext) -> Vec { - vec![ReportingProductId { - name: "AllTransactionsExceptRetainedEarnings", - kind: self.product_kinds[0], - args: self.args.clone(), - }] + vec![ + // AllTransactionsIncludingRetainedEarnings requires AllTransactionsExceptRetainedEarnings + ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + // AllTransactionsIncludingRetainedEarnings requires RetainedEarningsToEquity + ReportingProductId { + name: "RetainedEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + ] } } From 349ecf3d76050b9d0d56972eeb7f08187542f80c Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:24:29 +1000 Subject: [PATCH 08/45] Fix off by one error in BalancesAtToBalancesBetween --- src/reporting/builders.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 1f89da1..9779e69 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -139,7 +139,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { name: self.step_name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { - date: self.args.date_start.clone(), + date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day }), }, ReportingProductId { From 58758b0cb31779b6e423386a2ddacf09cb615dc9 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:24:59 +1000 Subject: [PATCH 09/45] Implement RetainedEarningsToEquity --- src/reporting/steps.rs | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index e98c162..85ab6a7 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -18,6 +18,8 @@ use std::fmt::Display; +use chrono::Datelike; + use crate::util::sofy_from_eofy; use super::{ @@ -82,6 +84,13 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { PostUnreconciledStatementLines::takes_args, PostUnreconciledStatementLines::from_args, ); + + context.register_lookup_fn( + "RetainedEarningsToEquity", + &[ReportingProductKind::Transactions], + RetainedEarningsToEquity::takes_args, + RetainedEarningsToEquity::from_args, + ); } #[derive(Debug)] @@ -349,3 +358,50 @@ impl ReportingStep for PostUnreconciledStatementLines { } } } + +#[derive(Debug)] +pub struct RetainedEarningsToEquity { + pub args: DateArgs, +} + +impl RetainedEarningsToEquity { + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(RetainedEarningsToEquity { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for RetainedEarningsToEquity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for RetainedEarningsToEquity { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "RetainedEarningsToEquity", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, context: &ReportingContext) -> Vec { + // RetainedEarningsToEquity depends on CombineOrdinaryTransactions for last financial year + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: context + .eofy_date + .with_year(context.eofy_date.year() - 1) + .unwrap(), + }), + }] + } +} From 37e9e19c5e8413c7e6a2bd2e156f831330486741 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 19:18:14 +1000 Subject: [PATCH 10/45] Rename Dependency.dependency to Dependency.product --- src/reporting/builders.rs | 10 +++--- src/reporting/calculator.rs | 61 +++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 9779e69..614f749 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -270,18 +270,18 @@ impl UpdateBalancesAt { // Check for BalancesAt -> Transactions let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); if dependencies_for_step.len() == 1 - && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesAt + && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesAt { return true; } // Check if BalancesBetween -> Transactions and BalancesAt is available if dependencies_for_step.len() == 1 - && dependencies_for_step[0].dependency.kind + && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesBetween { let date_end = dependencies_for_step[0] - .dependency + .product .args .downcast_ref::() .unwrap() @@ -289,7 +289,7 @@ impl UpdateBalancesAt { match has_step_or_can_build( &ReportingProductId { - name: dependencies_for_step[0].dependency.name, + name: dependencies_for_step[0].product.name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { date: date_end }), }, @@ -398,7 +398,7 @@ impl UpdateBalancesBetween { // Check for BalancesBetween -> Transactions let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); if dependencies_for_step.len() == 1 - && dependencies_for_step[0].dependency.kind + && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesBetween { return true; diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index dc7d11e..a06369b 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -35,9 +35,9 @@ impl ReportingGraphDependencies { if !self .vec .iter() - .any(|d| d.step == step && d.dependency == dependency) + .any(|d| d.step == step && d.product == dependency) { - self.vec.push(Dependency { step, dependency }); + self.vec.push(Dependency { step, product: dependency }); } } @@ -65,10 +65,11 @@ impl ReportingGraphDependencies { } } +/// Represents that a [ReportingStep] depends on a [ReportingProduct] #[derive(Debug)] pub struct Dependency { pub step: ReportingStepId, - pub dependency: ReportingProductId, + pub product: ReportingProductId, } #[derive(Debug)] @@ -147,12 +148,12 @@ fn would_be_ready_to_execute( // Check if the dependency has been produced by a previous step for previous_step in previous_steps { - if steps[*previous_step].id().name == dependency.dependency.name - && steps[*previous_step].id().args == dependency.dependency.args + if steps[*previous_step].id().name == dependency.product.name + && steps[*previous_step].id().args == dependency.product.args && steps[*previous_step] .id() .product_kinds - .contains(&dependency.dependency.kind) + .contains(&dependency.product.kind) { continue 'check_each_dependency; } @@ -197,33 +198,33 @@ pub fn solve_for( todo!(); } if !steps.iter().any(|s| { - s.id().name == dependency.dependency.name - && s.id().args == dependency.dependency.args - && s.id().product_kinds.contains(&dependency.dependency.kind) + s.id().name == dependency.product.name + && s.id().args == dependency.product.args + && s.id().product_kinds.contains(&dependency.product.kind) }) { // Try lookup function if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { - *name == dependency.dependency.name - && kinds.contains(&dependency.dependency.kind) + *name == dependency.product.name + && kinds.contains(&dependency.product.kind) }) { let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); - if takes_args_fn(&dependency.dependency.args) { - let new_step = from_args_fn(dependency.dependency.args.clone()); + if takes_args_fn(&dependency.product.args) { + let new_step = from_args_fn(dependency.product.args.clone()); // Check new step meets the dependency - if new_step.id().name != dependency.dependency.name { - panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + if new_step.id().name != dependency.product.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.product.name, new_step.id().name); } - if new_step.id().args != dependency.dependency.args { - panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + if new_step.id().args != dependency.product.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.product.name, dependency.product.args, new_step.id().args); } if !new_step .id() .product_kinds - .contains(&dependency.dependency.kind) + .contains(&dependency.product.kind) { - panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.product.name, dependency.product.kind, new_step.id().product_kinds); } new_steps.push(new_step); @@ -234,17 +235,17 @@ pub fn solve_for( // No explicit step for product - try builders for builder in context.step_dynamic_builders.iter() { if (builder.can_build)( - dependency.dependency.name, - dependency.dependency.kind, - &dependency.dependency.args, + dependency.product.name, + dependency.product.kind, + &dependency.product.args, &steps, &dependencies, &context, ) { new_steps.push((builder.build)( - dependency.dependency.name, - dependency.dependency.kind, - dependency.dependency.args.clone(), + dependency.product.name, + dependency.product.kind, + dependency.product.args.clone(), &steps, &dependencies, &context, @@ -283,19 +284,19 @@ pub fn solve_for( return Err(ReportingCalculationError::UnknownStep { message: format!( "No implementation for step {} which {} is a dependency of", - dependency.step, dependency.dependency + dependency.step, dependency.product ), }); } if !steps.iter().any(|s| { - s.id().name == dependency.dependency.name - && s.id().args == dependency.dependency.args - && s.id().product_kinds.contains(&dependency.dependency.kind) + s.id().name == dependency.product.name + && s.id().args == dependency.product.args + && s.id().product_kinds.contains(&dependency.product.kind) }) { return Err(ReportingCalculationError::NoStepForProduct { message: format!( "No step builds product {} wanted by {}", - dependency.dependency, dependency.step + dependency.product, dependency.step ), }); } From ae26b64d5ee6e3b6e4c5a317a6562ae9c4756115 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 19:59:57 +1000 Subject: [PATCH 11/45] Refactoring and documentation --- src/main.rs | 18 +-- src/reporting/builders.rs | 4 +- src/reporting/calculator.rs | 65 ++++----- src/reporting/mod.rs | 201 +------------------------ src/reporting/steps.rs | 8 +- src/reporting/types.rs | 282 ++++++++++++++++++++++++++++++++++++ 6 files changed, 323 insertions(+), 255 deletions(-) create mode 100644 src/reporting/types.rs diff --git a/src/main.rs b/src/main.rs index b112c11..16f2c3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,13 +17,13 @@ */ use chrono::NaiveDate; -use libdrcr::reporting::{ - builders::register_dynamic_builders, - calculator::solve_for, - steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, - AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, - }, +use libdrcr::reporting::builders::register_dynamic_builders; +use libdrcr::reporting::calculator::steps_for_targets; +use libdrcr::reporting::steps::{ + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, +}; +use libdrcr::reporting::types::{ DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, }; @@ -44,7 +44,7 @@ fn main() { ]; println!("For income statement:"); - match solve_for(targets, context) { + match steps_for_targets(targets, context) { Ok(steps) => { for step in steps { println!("- {}", step); @@ -67,7 +67,7 @@ fn main() { ]; println!("For balance sheet:"); - match solve_for(targets, context) { + match steps_for_targets(targets, context) { Ok(steps) => { for step in steps { println!("- {}", step); diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 614f749..15bb630 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -18,8 +18,8 @@ use std::fmt::Display; -use super::{ - calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, +use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; +use super::types::{ DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index a06369b..47f9c0e 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -16,50 +16,37 @@ along with this program. If not, see . */ -use super::{ +//! This module implements the dependency resolution for [ReportingStep]s + +use super::types::{ ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, }; +/// List of dependencies between [ReportingStep]s and [ReportingProduct]s #[derive(Debug)] pub struct ReportingGraphDependencies { vec: Vec, } impl ReportingGraphDependencies { + /// Get the list of [Dependency]s pub fn vec(&self) -> &Vec { &self.vec } - pub fn add_dependency(&mut self, step: ReportingStepId, dependency: ReportingProductId) { + /// Record that the [ReportingStep] depends on the [ReportingProduct] + pub fn add_dependency(&mut self, step: ReportingStepId, product: ReportingProductId) { if !self .vec .iter() - .any(|d| d.step == step && d.product == dependency) + .any(|d| d.step == step && d.product == product) { - self.vec.push(Dependency { step, product: dependency }); + self.vec.push(Dependency { step, product }); } } - /*pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { - for kind in target.product_kinds { - match kind { - ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { - self.add_dependency( - target.clone(), - ReportingProductId { - name: dependency.name, - kind: *kind, - args: target.args.clone(), - }, - ); - } - ReportingProductKind::BalancesAt => todo!(), - ReportingProductKind::Generic => todo!(), - } - } - }*/ - + /// Get the [Dependency]s for the given [ReportingStep] pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { return self.vec.iter().filter(|d| d.step == *step).collect(); } @@ -72,6 +59,7 @@ pub struct Dependency { pub product: ReportingProductId, } +/// Indicates an error during dependency resolution in [steps_for_targets] #[derive(Debug)] pub enum ReportingCalculationError { UnknownStep { message: String }, @@ -86,6 +74,7 @@ pub enum HasStepOrCanBuild<'a, 'b> { None, } +/// Determines whether the [ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder pub fn has_step_or_can_build<'a, 'b>( product: &ReportingProductId, steps: &'a Vec>, @@ -129,23 +118,15 @@ pub fn has_step_or_can_build<'a, 'b>( return HasStepOrCanBuild::None; } +/// Check whether the [ReportingStep] would be ready to execute, if the given previous steps have already completed fn would_be_ready_to_execute( step: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, previous_steps: &Vec, ) -> bool { - //println!( - // "- would_be_ready_to_execute: {}, {:?}", - // step.id(), - // previous_steps - //); - - // Check whether the step would be ready to execute, if the previous steps have already completed 'check_each_dependency: for dependency in dependencies.vec.iter() { if dependency.step == step.id() { - //println!("-- {}", dependency.dependency); - // Check if the dependency has been produced by a previous step for previous_step in previous_steps { if steps[*previous_step].id().name == dependency.product.name @@ -166,7 +147,8 @@ fn would_be_ready_to_execute( true } -pub fn solve_for( +/// Recursively resolve the dependencies of the target [ReportingStep]s and return a sorted [Vec] of [ReportingStep]s +pub fn steps_for_targets( targets: Vec>, context: ReportingContext, ) -> Result>, ReportingCalculationError> { @@ -180,12 +162,15 @@ pub fn solve_for( for dependency in target.requires(&context) { dependencies.add_dependency(target.id(), dependency); } - target.as_ref().init_graph(&steps, &mut dependencies, &context); + target + .as_ref() + .init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on targets for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies, &context); + step.as_ref() + .after_init_graph(&steps, &mut dependencies, &context); } // Process dependencies @@ -204,8 +189,7 @@ pub fn solve_for( }) { // Try lookup function if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { - *name == dependency.product.name - && kinds.contains(&dependency.product.kind) + *name == dependency.product.name && kinds.contains(&dependency.product.kind) }) { let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); @@ -269,12 +253,15 @@ pub fn solve_for( for dependency in new_step.requires(&context) { dependencies.add_dependency(new_step.id(), dependency); } - new_step.as_ref().init_graph(&steps, &mut dependencies, &context); + new_step + .as_ref() + .init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on all steps for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies, &context); + step.as_ref() + .after_init_graph(&steps, &mut dependencies, &context); } } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 5a1ae40..bc34ccb 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -16,206 +16,7 @@ along with this program. If not, see . */ -use std::fmt::Debug; -use std::{collections::HashMap, fmt::Display}; - -use calculator::ReportingGraphDependencies; -use chrono::NaiveDate; -use downcast_rs::Downcast; -use dyn_clone::DynClone; -use dyn_eq::DynEq; - pub mod builders; pub mod calculator; pub mod steps; - -pub struct ReportingContext { - eofy_date: NaiveDate, - step_lookup_fn: HashMap< - (&'static str, &'static [ReportingProductKind]), - (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), - >, - step_dynamic_builders: Vec, -} - -impl ReportingContext { - pub fn new(eofy_date: NaiveDate) -> Self { - Self { - eofy_date: eofy_date, - step_lookup_fn: HashMap::new(), - step_dynamic_builders: Vec::new(), - } - } - - fn register_lookup_fn( - &mut self, - name: &'static str, - product_kinds: &'static [ReportingProductKind], - takes_args_fn: ReportingStepTakesArgsFn, - from_args_fn: ReportingStepFromArgsFn, - ) { - self.step_lookup_fn - .insert((name, product_kinds), (takes_args_fn, from_args_fn)); - } - - fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { - if !self - .step_dynamic_builders - .iter() - .any(|b| b.name == builder.name) - { - self.step_dynamic_builders.push(builder); - } - } -} - -#[derive(Debug, Eq, PartialEq)] -pub struct ReportingProductId { - name: &'static str, - kind: ReportingProductKind, - args: Box, -} - -impl Display for ReportingProductId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args)) - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum ReportingProductKind { - Transactions, - BalancesAt, - BalancesBetween, - Generic, -} - -//enum ReportingProduct { -// Transactions(Transactions), -// BalancesAt(BalancesAt), -// BalancesBetween(BalancesBetween), -// Generic(Box), -//} - -//struct Transactions {} -//struct BalancesAt {} -//struct BalancesBetween {} - -//trait GenericReportingProduct {} - -//type ReportingProducts = HashMap; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ReportingStepId { - pub name: &'static str, - pub product_kinds: &'static [ReportingProductKind], - pub args: Box, -} - -impl Display for ReportingStepId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "{}{:?}({})", - self.name, self.product_kinds, self.args - )) - } -} - -pub trait ReportingStep: Debug + Display + Downcast { - // Info - fn id(&self) -> ReportingStepId; - - // Methods - fn requires(&self, _context: &ReportingContext) -> Vec { - vec![] - } - - fn init_graph( - &self, - _steps: &Vec>, - _dependencies: &mut ReportingGraphDependencies, - _context: &ReportingContext, - ) { - } - - fn after_init_graph( - &self, - _steps: &Vec>, - _dependencies: &mut ReportingGraphDependencies, - _context: &ReportingContext, - ) { - } - - //fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) { - // todo!(); - //} -} - -downcast_rs::impl_downcast!(ReportingStep); - -pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq {} - -downcast_rs::impl_downcast!(ReportingStepArgs); -dyn_clone::clone_trait_object!(ReportingStepArgs); -dyn_eq::eq_trait_object!(ReportingStepArgs); - -pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; -pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct VoidArgs {} - -impl ReportingStepArgs for VoidArgs {} - -impl Display for VoidArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("")) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DateArgs { - pub date: NaiveDate, -} - -impl ReportingStepArgs for DateArgs {} - -impl Display for DateArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.date)) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DateStartDateEndArgs { - pub date_start: NaiveDate, - pub date_end: NaiveDate, -} - -impl ReportingStepArgs for DateStartDateEndArgs {} - -impl Display for DateStartDateEndArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) - } -} - -pub struct ReportingStepDynamicBuilder { - name: &'static str, - can_build: fn( - name: &'static str, - kind: ReportingProductKind, - args: &Box, - steps: &Vec>, - dependencies: &ReportingGraphDependencies, - context: &ReportingContext, - ) -> bool, - build: fn( - name: &'static str, - kind: ReportingProductKind, - args: Box, - steps: &Vec>, - dependencies: &ReportingGraphDependencies, - context: &ReportingContext, - ) -> Box, -} +pub mod types; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 85ab6a7..15482cd 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -20,13 +20,11 @@ use std::fmt::Display; use chrono::Datelike; +use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId}; use crate::util::sofy_from_eofy; -use super::{ - calculator::ReportingGraphDependencies, DateArgs, DateStartDateEndArgs, ReportingContext, - ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepId, - VoidArgs, -}; +use super:: calculator::ReportingGraphDependencies; +use super::types::{DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs}; pub fn register_lookup_fns(context: &mut ReportingContext) { context.register_lookup_fn( diff --git a/src/reporting/types.rs b/src/reporting/types.rs new file mode 100644 index 0000000..e6a25bd --- /dev/null +++ b/src/reporting/types.rs @@ -0,0 +1,282 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use std::collections::HashMap; +use std::fmt::{Debug, Display}; + +use chrono::NaiveDate; +use downcast_rs::Downcast; +use dyn_clone::DynClone; +use dyn_eq::DynEq; +use dyn_hash::DynHash; + +use crate::QuantityInt; + +use super::calculator::ReportingGraphDependencies; + +// ----------------- +// REPORTING CONTEXT + +/// Records the context for a single reporting job +pub struct ReportingContext { + pub eofy_date: NaiveDate, + pub(crate) step_lookup_fn: HashMap< + (&'static str, &'static [ReportingProductKind]), + (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), + >, + pub(crate) step_dynamic_builders: Vec, +} + +impl ReportingContext { + /// Initialise a new [ReportingContext] + pub fn new(eofy_date: NaiveDate) -> Self { + Self { + eofy_date: eofy_date, + step_lookup_fn: HashMap::new(), + step_dynamic_builders: Vec::new(), + } + } + + /// Register a lookup function + /// + /// A lookup function generates concrete [ReportingStep]s from a [ReportingStepId]. + pub fn register_lookup_fn( + &mut self, + name: &'static str, + product_kinds: &'static [ReportingProductKind], + takes_args_fn: ReportingStepTakesArgsFn, + from_args_fn: ReportingStepFromArgsFn, + ) { + self.step_lookup_fn + .insert((name, product_kinds), (takes_args_fn, from_args_fn)); + } + + /// Register a dynamic builder + /// + /// Dynamic builders are called when no concrete [ReportingStep] is implemented, and can dynamically generate a [ReportingStep]. Dynamic builders are implemented in [super::builders]. + pub fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { + if !self + .step_dynamic_builders + .iter() + .any(|b| b.name == builder.name) + { + self.step_dynamic_builders.push(builder); + } + } +} + +/// Function which determines whether the [ReportingStepArgs] are valid arguments for a given [ReportingStep] +/// +/// See [ReportingContext::register_lookup_fn]. +pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; + +/// Function which builds a concrete [ReportingStep] from the given [ReportingStepArgs] +/// +/// See [ReportingContext::register_lookup_fn]. +pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; + +// ------------------------------- +// REPORTING STEP DYNAMIC BUILDERS + +/// Represents a reporting step dynamic builder +/// +/// See [ReportingContext::register_dynamic_builder]. +pub struct ReportingStepDynamicBuilder { + pub name: &'static str, + pub can_build: fn( + name: &'static str, + kind: ReportingProductKind, + args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool, + pub build: fn( + name: &'static str, + kind: ReportingProductKind, + args: Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> Box, +} + +// ------------------ +// REPORTING PRODUCTS + +/// Identifies a [ReportingProduct] +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct ReportingProductId { + pub name: &'static str, + pub kind: ReportingProductKind, + pub args: Box, +} + +impl Display for ReportingProductId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args)) + } +} + +/// Identifies a type of [ReportingProduct] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ReportingProductKind { + Transactions, + BalancesAt, + BalancesBetween, + Generic, +} + +/// Represents the result of a [ReportingStep] +pub enum ReportingProduct { + Transactions(Transactions), + BalancesAt(BalancesAt), + BalancesBetween(BalancesBetween), + Generic(Box), +} + +/// Records a list of transactions generated by a [ReportingStep] +pub struct Transactions {} + +/// Records cumulative account balances at a particular point in time +pub struct BalancesAt { + pub balances: HashMap, +} + +/// Records the total value of transactions in each account between two points in time +pub struct BalancesBetween {} + +/// Represents a custom [ReportingProduct] generated by a [ReportingStep] +pub trait GenericReportingProduct {} + +/// Convenience type mapping [ReportingProductId] to [ReportingProduct] +pub type ReportingProducts = HashMap; + +// --------------- +// REPORTING STEPS + +/// Identifies a [ReportingStep] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReportingStepId { + pub name: &'static str, + pub product_kinds: &'static [ReportingProductKind], + pub args: Box, +} + +impl Display for ReportingStepId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}{:?}({})", + self.name, self.product_kinds, self.args + )) + } +} + +/// Represents a step in a reporting job +pub trait ReportingStep: Debug + Display + Downcast { + /// Get the [ReportingStepId] for this [ReportingStep] + fn id(&self) -> ReportingStepId; + + /// Return a list of statically defined dependencies for this [ReportingStep] + #[allow(unused_variables)] + fn requires(&self, context: &ReportingContext) -> Vec { + vec![] + } + + /// Called when the [ReportingStep] is initialised in [super::calculator::steps_for_targets] + #[allow(unused_variables)] + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + context: &ReportingContext, + ) { + } + + /// Called when new [ReportingStep]s are initialised in [super::calculator::steps_for_targets] + /// + /// This callback can be used to dynamically declare dependencies between [ReportingStep]s that are not known at initialisation. + #[allow(unused_variables)] + fn after_init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + context: &ReportingContext, + ) { + } + + /// Called to generate the [ReportingProduct] for this [ReportingStep] + #[allow(unused_variables)] + fn execute(&self, context: &ReportingContext, products: &mut ReportingProducts) { + todo!(); + } +} + +downcast_rs::impl_downcast!(ReportingStep); + +// ------------------------ +// REPORTING STEP ARGUMENTS + +/// Represents arguments to a [ReportingStep] +pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq + DynHash {} + +downcast_rs::impl_downcast!(ReportingStepArgs); +dyn_clone::clone_trait_object!(ReportingStepArgs); +dyn_eq::eq_trait_object!(ReportingStepArgs); +dyn_hash::hash_trait_object!(ReportingStepArgs); + +/// [ReportingStepArgs] implementation which takes no arguments +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VoidArgs {} + +impl ReportingStepArgs for VoidArgs {} + +impl Display for VoidArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("")) + } +} + +/// [ReportingStepArgs] implementation which takes a single date +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct DateArgs { + pub date: NaiveDate, +} + +impl ReportingStepArgs for DateArgs {} + +impl Display for DateArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.date)) + } +} + +/// [ReportingStepArgs] implementation which takes a date range +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct DateStartDateEndArgs { + pub date_start: NaiveDate, + pub date_end: NaiveDate, +} + +impl ReportingStepArgs for DateStartDateEndArgs {} + +impl Display for DateStartDateEndArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) + } +} From 0f8e3e5d4a84fe24655a4941842c817d2ea34b18 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 20:15:18 +1000 Subject: [PATCH 12/45] Basic framework for executing reports --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/lib.rs | 2 ++ src/main.rs | 40 +++++-------------------------------- src/reporting/calculator.rs | 2 +- src/reporting/executor.rs | 37 ++++++++++++++++++++++++++++++++++ src/reporting/mod.rs | 36 +++++++++++++++++++++++++++++++++ src/reporting/steps.rs | 36 ++++++++++++++++++++++++++++++--- src/reporting/types.rs | 15 +++++++++++--- 9 files changed, 134 insertions(+), 42 deletions(-) create mode 100644 src/reporting/executor.rs diff --git a/Cargo.lock b/Cargo.lock index fee6985..14f6134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" +[[package]] +name = "dyn-hash" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -130,6 +136,7 @@ dependencies = [ "downcast-rs", "dyn-clone", "dyn-eq", + "dyn-hash", "solvent", ] diff --git a/Cargo.toml b/Cargo.toml index b4bac49..9cd9b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ chrono = "0.4.41" downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" +dyn-hash = "0.2.2" solvent = "0.8.3" diff --git a/src/lib.rs b/src/lib.rs index 24e0b08..6130654 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ pub mod reporting; pub mod transaction; pub mod util; + +pub type QuantityInt = u64; diff --git a/src/main.rs b/src/main.rs index 16f2c3a..65cc9e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,13 +18,12 @@ use chrono::NaiveDate; use libdrcr::reporting::builders::register_dynamic_builders; -use libdrcr::reporting::calculator::steps_for_targets; +use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, - AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, + register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax, }; use libdrcr::reporting::types::{ - DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, + DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, }; fn main() { @@ -43,36 +42,7 @@ fn main() { }), ]; - println!("For income statement:"); - match steps_for_targets(targets, context) { - Ok(steps) => { - for step in steps { - println!("- {}", step); - } - } - Err(err) => panic!("Error: {:?}", err), - } + let products = generate_report(targets, &context); - let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); - register_lookup_fns(&mut context); - register_dynamic_builders(&mut context); - - let targets: Vec> = vec![ - Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsIncludingRetainedEarnings { - args: DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), - ]; - - println!("For balance sheet:"); - match steps_for_targets(targets, context) { - Ok(steps) => { - for step in steps { - println!("- {}", step); - } - } - Err(err) => panic!("Error: {:?}", err), - } + println!("{:?}", products); } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 47f9c0e..68cbbfb 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -150,7 +150,7 @@ fn would_be_ready_to_execute( /// Recursively resolve the dependencies of the target [ReportingStep]s and return a sorted [Vec] of [ReportingStep]s pub fn steps_for_targets( targets: Vec>, - context: ReportingContext, + context: &ReportingContext, ) -> Result>, ReportingCalculationError> { let mut steps: Vec> = Vec::new(); let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs new file mode 100644 index 0000000..bcb9976 --- /dev/null +++ b/src/reporting/executor.rs @@ -0,0 +1,37 @@ +/* + DrCr: Web-based double-entry bookkeeping framework + Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use super::types::{ReportingContext, ReportingProducts, ReportingStep}; + +#[derive(Debug)] +pub struct ReportingExecutionError { + message: String, +} + +pub fn execute_steps( + steps: Vec>, + context: &ReportingContext, +) -> Result { + let mut products = ReportingProducts::new(); + + for step in steps { + step.execute(context, &mut products)?; + } + + Ok(products) +} diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index bc34ccb..83a672f 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -16,7 +16,43 @@ along with this program. If not, see . */ +use calculator::{steps_for_targets, ReportingCalculationError}; +use executor::{execute_steps, ReportingExecutionError}; +use types::{ReportingContext, ReportingProducts, ReportingStep}; + pub mod builders; pub mod calculator; +pub mod executor; pub mod steps; pub mod types; + +#[derive(Debug)] +pub enum ReportingError { + ReportingCalculationError(ReportingCalculationError), + ReportingExecutionError(ReportingExecutionError), +} + +impl From for ReportingError { + fn from(err: ReportingCalculationError) -> Self { + ReportingError::ReportingCalculationError(err) + } +} + +impl From for ReportingError { + fn from(err: ReportingExecutionError) -> Self { + ReportingError::ReportingExecutionError(err) + } +} + +pub fn generate_report( + targets: Vec>, + context: &ReportingContext, +) -> Result { + // Solve dependencies + let sorted_steps = steps_for_targets(targets, context)?; + + // Execute steps + let products = execute_steps(sorted_steps, context)?; + + Ok(products) +} diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 15482cd..9984f69 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,15 +16,22 @@ along with this program. If not, see . */ +use std::collections::HashMap; use std::fmt::Display; use chrono::Datelike; -use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId}; +use crate::reporting::types::{ + BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId, +}; use crate::util::sofy_from_eofy; -use super:: calculator::ReportingGraphDependencies; -use super::types::{DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs}; +use super::calculator::ReportingGraphDependencies; +use super::executor::ReportingExecutionError; +use super::types::{ + DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, + ReportingStepArgs, ReportingStepId, VoidArgs, +}; pub fn register_lookup_fns(context: &mut ReportingContext) { context.register_lookup_fn( @@ -322,6 +329,29 @@ impl ReportingStep for DBBalances { args: Box::new(self.args.clone()), } } + + fn execute( + &self, + _context: &ReportingContext, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: DBBalances.execute"); + + let balances = BalancesAt { + balances: HashMap::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + ReportingProduct::BalancesAt(balances), + ); + + Ok(()) + } } #[derive(Debug)] diff --git a/src/reporting/types.rs b/src/reporting/types.rs index e6a25bd..7272eb3 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -28,6 +28,7 @@ use dyn_hash::DynHash; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; +use super::executor::ReportingExecutionError; // ----------------- // REPORTING CONTEXT @@ -143,6 +144,7 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] +#[derive(Debug)] pub enum ReportingProduct { Transactions(Transactions), BalancesAt(BalancesAt), @@ -151,18 +153,21 @@ pub enum ReportingProduct { } /// Records a list of transactions generated by a [ReportingStep] +#[derive(Debug)] pub struct Transactions {} /// Records cumulative account balances at a particular point in time +#[derive(Debug)] pub struct BalancesAt { pub balances: HashMap, } /// Records the total value of transactions in each account between two points in time +#[derive(Debug)] pub struct BalancesBetween {} /// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct {} +pub trait GenericReportingProduct: Debug {} /// Convenience type mapping [ReportingProductId] to [ReportingProduct] pub type ReportingProducts = HashMap; @@ -222,8 +227,12 @@ pub trait ReportingStep: Debug + Display + Downcast { /// Called to generate the [ReportingProduct] for this [ReportingStep] #[allow(unused_variables)] - fn execute(&self, context: &ReportingContext, products: &mut ReportingProducts) { - todo!(); + fn execute( + &self, + context: &ReportingContext, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + todo!("{}", self); } } From 7f188db677f5bbea062a8d877411fee2908eb69d Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 20:22:06 +1000 Subject: [PATCH 13/45] Refactor register_lookup_fns and register_dynamic_builders for readability --- src/reporting/builders.rs | 61 ++++++++++------- src/reporting/steps.rs | 140 +++++++++++++++++++++----------------- 2 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 15bb630..0eaa902 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -24,31 +24,14 @@ use super::types::{ ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; +/// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module pub fn register_dynamic_builders(context: &mut ReportingContext) { - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "GenerateBalances", - can_build: GenerateBalances::can_build, - build: GenerateBalances::build, - }); + GenerateBalances::register_dynamic_builder(context); + UpdateBalancesBetween::register_dynamic_builder(context); + UpdateBalancesAt::register_dynamic_builder(context); - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "UpdateBalancesBetween", - can_build: UpdateBalancesBetween::can_build, - build: UpdateBalancesBetween::build, - }); - - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "UpdateBalancesAt", - can_build: UpdateBalancesAt::can_build, - build: UpdateBalancesAt::build, - }); - - // This is the least efficient way of generating BalancesBetween - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "BalancesAtToBalancesBetween", - can_build: BalancesAtToBalancesBetween::can_build, - build: BalancesAtToBalancesBetween::build, - }); + // This is the least efficient way of generating BalancesBetween so put at the end + BalancesAtToBalancesBetween::register_dynamic_builder(context); } #[derive(Debug)] @@ -60,6 +43,14 @@ pub struct BalancesAtToBalancesBetween { impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, @@ -162,6 +153,14 @@ pub struct GenerateBalances { impl GenerateBalances { // Implements (() -> Transactions) -> BalancesAt + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "GenerateBalances", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, @@ -250,6 +249,14 @@ pub struct UpdateBalancesAt { impl UpdateBalancesAt { // Implements (BalancesAt -> Transactions) -> BalancesAt + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesAt", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, @@ -378,6 +385,14 @@ pub struct UpdateBalancesBetween { impl UpdateBalancesBetween { // Implements (BalancesBetween -> Transactions) -> BalancesBetween + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesBetween", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 9984f69..e021277 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -33,69 +33,15 @@ use super::types::{ ReportingStepArgs, ReportingStepId, VoidArgs, }; +/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module pub fn register_lookup_fns(context: &mut ReportingContext) { - context.register_lookup_fn( - "AllTransactionsExceptRetainedEarnings", - &[ReportingProductKind::BalancesAt], - AllTransactionsExceptRetainedEarnings::takes_args, - |a| { - AllTransactionsExceptRetainedEarnings::from_args(&[ReportingProductKind::BalancesAt], a) - }, - ); - - context.register_lookup_fn( - "AllTransactionsExceptRetainedEarnings", - &[ReportingProductKind::BalancesBetween], - AllTransactionsExceptRetainedEarnings::takes_args, - |a| { - AllTransactionsExceptRetainedEarnings::from_args( - &[ReportingProductKind::BalancesBetween], - a, - ) - }, - ); - - context.register_lookup_fn( - "AllTransactionsIncludingRetainedEarnings", - &[ReportingProductKind::BalancesAt], - AllTransactionsIncludingRetainedEarnings::takes_args, - AllTransactionsIncludingRetainedEarnings::from_args, - ); - - context.register_lookup_fn( - "CalculateIncomeTax", - &[ReportingProductKind::Transactions], - CalculateIncomeTax::takes_args, - CalculateIncomeTax::from_args, - ); - - context.register_lookup_fn( - "CombineOrdinaryTransactions", - &[ReportingProductKind::BalancesAt], - CombineOrdinaryTransactions::takes_args, - CombineOrdinaryTransactions::from_args, - ); - - context.register_lookup_fn( - "DBBalances", - &[ReportingProductKind::BalancesAt], - DBBalances::takes_args, - DBBalances::from_args, - ); - - context.register_lookup_fn( - "PostUnreconciledStatementLines", - &[ReportingProductKind::Transactions], - PostUnreconciledStatementLines::takes_args, - PostUnreconciledStatementLines::from_args, - ); - - context.register_lookup_fn( - "RetainedEarningsToEquity", - &[ReportingProductKind::Transactions], - RetainedEarningsToEquity::takes_args, - RetainedEarningsToEquity::from_args, - ); + AllTransactionsExceptRetainedEarnings::register_lookup_fn(context); + AllTransactionsIncludingRetainedEarnings::register_lookup_fn(context); + CalculateIncomeTax::register_lookup_fn(context); + CombineOrdinaryTransactions::register_lookup_fn(context); + DBBalances::register_lookup_fn(context); + PostUnreconciledStatementLines::register_lookup_fn(context); + RetainedEarningsToEquity::register_lookup_fn(context); } #[derive(Debug)] @@ -105,6 +51,22 @@ pub struct AllTransactionsExceptRetainedEarnings { } impl AllTransactionsExceptRetainedEarnings { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + |a| Self::from_args(&[ReportingProductKind::BalancesAt], a), + ); + + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + Self::takes_args, + |a| Self::from_args(&[ReportingProductKind::BalancesBetween], a), + ); + } + fn takes_args(_args: &Box) -> bool { true } @@ -142,6 +104,15 @@ pub struct AllTransactionsIncludingRetainedEarnings { } impl AllTransactionsIncludingRetainedEarnings { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -190,6 +161,15 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { pub struct CalculateIncomeTax {} impl CalculateIncomeTax { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "CalculateIncomeTax", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(_args: &Box) -> bool { true } @@ -254,6 +234,15 @@ pub struct CombineOrdinaryTransactions { } impl CombineOrdinaryTransactions { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "CombineOrdinaryTransactions", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -304,6 +293,15 @@ pub struct DBBalances { } impl DBBalances { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "DBBalances", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -360,6 +358,15 @@ pub struct PostUnreconciledStatementLines { } impl PostUnreconciledStatementLines { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "PostUnreconciledStatementLines", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -393,6 +400,15 @@ pub struct RetainedEarningsToEquity { } impl RetainedEarningsToEquity { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "RetainedEarningsToEquity", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } From bfb41d8d15387c966ff32689ccd61f47c61fc2c5 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 21:48:57 +1000 Subject: [PATCH 14/45] Stub implementations for all steps --- Cargo.lock | 30 +++- Cargo.toml | 2 +- src/main.rs | 46 +++++- src/reporting/builders.rs | 297 +++++++++++++++++++++++++++++++++--- src/reporting/calculator.rs | 4 +- src/reporting/executor.rs | 11 +- src/reporting/mod.rs | 4 +- src/reporting/steps.rs | 241 ++++++++++++++++++++++++++++- src/reporting/types.rs | 87 +++++++++-- src/transaction.rs | 25 ++- 10 files changed, 676 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14f6134..7a70601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -112,6 +124,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -137,7 +159,7 @@ dependencies = [ "dyn-clone", "dyn-eq", "dyn-hash", - "solvent", + "indexmap", ] [[package]] @@ -191,12 +213,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "solvent" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" - [[package]] name = "syn" version = "2.0.101" diff --git a/Cargo.toml b/Cargo.toml index 9cd9b5d..f6046dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" -solvent = "0.8.3" +indexmap = "2.9.0" diff --git a/src/main.rs b/src/main.rs index 65cc9e2..b7e2bc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,10 +20,12 @@ use chrono::NaiveDate; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax, + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, }; use libdrcr::reporting::types::{ - DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, + ReportingStep, }; fn main() { @@ -31,6 +33,8 @@ fn main() { register_lookup_fns(&mut context); register_dynamic_builders(&mut context); + // Get income statement + let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsExceptRetainedEarnings { @@ -42,7 +46,41 @@ fn main() { }), ]; - let products = generate_report(targets, &context); + let products = generate_report(targets, &context).unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }) + .unwrap(); - println!("{:?}", products); + println!("{:?}", result); + + // Get balance sheet + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax {}), + Box::new(AllTransactionsIncludingRetainedEarnings { + args: DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, + }), + ]; + + let products = generate_report(targets, &context).unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsIncludingRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }) + .unwrap(); + + println!("{:?}", result); } diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 0eaa902..8cd3c11 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,12 +16,15 @@ along with this program. If not, see . */ +use std::collections::HashMap; use std::fmt::Display; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; +use super::executor::ReportingExecutionError; use super::types::{ - DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, - ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, + BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, + ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, + ReportingStepDynamicBuilder, ReportingStepId, Transactions, }; /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module @@ -40,6 +43,7 @@ pub struct BalancesAtToBalancesBetween { args: DateStartDateEndArgs, } +/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween @@ -137,13 +141,69 @@ impl ReportingStep for BalancesAtToBalancesBetween { name: self.step_name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { - date: self.args.date_end.clone(), + date: self.args.date_end, }), }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get balances at dates + let balances_start = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day + }), + })? + .downcast_ref::() + .unwrap() + .balances; + + let balances_end = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date_end, + }), + })? + .downcast_ref::() + .unwrap() + .balances; + + // Compute balances_end - balances_start + let mut balances = BalancesBetween { + balances: balances_end.clone(), + }; + + for (account, balance) in balances_start.iter() { + let running_balance = balances.balances.get(account).unwrap_or(&0) - balance; + balances.balances.insert(account.clone(), running_balance); + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [super::steps::PostUnreconciledStatementLines]) #[derive(Debug)] pub struct GenerateBalances { step_name: &'static str, @@ -151,8 +211,6 @@ pub struct GenerateBalances { } impl GenerateBalances { - // Implements (() -> Transactions) -> BalancesAt - fn register_dynamic_builder(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", @@ -238,8 +296,58 @@ impl ReportingStep for GenerateBalances { args: Box::new(self.args.clone()), }] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get the transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Sum balances + let mut balances = BalancesAt { + balances: HashMap::new(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesAt] from: +/// - a step which generates [Transactions] from [BalancesAt], or +/// - a step which generates [Transactions] from [BalancesBetween], and for which a [BalancesAt] is also available #[derive(Debug)] pub struct UpdateBalancesAt { step_name: &'static str, @@ -374,8 +482,97 @@ impl ReportingStep for UpdateBalancesAt { }, ); } + + fn execute( + &self, + _context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Look up the parent step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + // Get transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args, + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Look up the BalancesAt step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let dependency = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build + + let opening_balances_at; + + if dependency.kind == ReportingProductKind::BalancesAt { + // Directly depends on BalancesAt -> Transaction + opening_balances_at = products + .get_or_err(&dependency)? + .downcast_ref::() + .unwrap(); + } else { + // As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available + let date_end = dependency + .args + .downcast_ref::() + .unwrap() + .date_end; + + opening_balances_at = products + .get_or_err(&ReportingProductId { + name: dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: date_end }), + })? + .downcast_ref() + .unwrap(); + } + + // Sum balances + let mut balances = BalancesAt { + balances: opening_balances_at.balances.clone(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesBetween] from a step which generates [Transactions] from [BalancesBetween] #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, @@ -383,8 +580,6 @@ pub struct UpdateBalancesBetween { } impl UpdateBalancesBetween { - // Implements (BalancesBetween -> Transactions) -> BalancesBetween - fn register_dynamic_builder(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesBetween", @@ -419,23 +614,6 @@ impl UpdateBalancesBetween { return true; } } - - // Check lookup or builder - with args - /*match has_step_or_can_build( - &ReportingProductId { - name, - kind: ReportingProductKind::Transactions, - args: args.clone(), - }, - steps, - dependencies, - context, - ) { - HasStepOrCanBuild::HasStep(step) => unreachable!(), - HasStepOrCanBuild::CanLookup(_) - | HasStepOrCanBuild::CanBuild(_) - | HasStepOrCanBuild::None => {} - }*/ } return false; } @@ -493,8 +671,77 @@ impl ReportingStep for UpdateBalancesBetween { ReportingProductId { name: self.step_name, kind: ReportingProductKind::Transactions, - args: parent_step.id().args.clone(), + args: parent_step.id().args, }, ); } + + fn execute( + &self, + _context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Look up the parent step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + // Get transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args, + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Look up the BalancesBetween step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness is checked in can_build + + // Get opening balances + let opening_balances = &products + .get_or_err(balances_between_product)? + .downcast_ref::() + .unwrap() + .balances; + + // Sum balances + let mut balances = BalancesBetween { + balances: opening_balances.clone(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 68cbbfb..862df18 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -151,7 +151,7 @@ fn would_be_ready_to_execute( pub fn steps_for_targets( targets: Vec>, context: &ReportingContext, -) -> Result>, ReportingCalculationError> { +) -> Result<(Vec>, ReportingGraphDependencies), ReportingCalculationError> { let mut steps: Vec> = Vec::new(); let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; @@ -319,5 +319,5 @@ pub fn steps_for_targets( .map(|(s, _idx)| s) .collect::>(); - Ok(sorted_steps) + Ok((sorted_steps, dependencies)) } diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index bcb9976..5c9dd4d 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -16,21 +16,22 @@ along with this program. If not, see . */ -use super::types::{ReportingContext, ReportingProducts, ReportingStep}; +use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}}; #[derive(Debug)] -pub struct ReportingExecutionError { - message: String, +pub enum ReportingExecutionError { + DependencyNotAvailable { message: String } } pub fn execute_steps( steps: Vec>, + dependencies: ReportingGraphDependencies, context: &ReportingContext, ) -> Result { let mut products = ReportingProducts::new(); - for step in steps { - step.execute(context, &mut products)?; + for step in steps.iter() { + step.execute(context, &steps, &dependencies, &mut products)?; } Ok(products) diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 83a672f..411a8ef 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -49,10 +49,10 @@ pub fn generate_report( context: &ReportingContext, ) -> Result { // Solve dependencies - let sorted_steps = steps_for_targets(targets, context)?; + let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; // Execute steps - let products = execute_steps(sorted_steps, context)?; + let products = execute_steps(sorted_steps, dependencies, context)?; Ok(products) } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index e021277..b541f4a 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,21 +16,21 @@ along with this program. If not, see . */ +//! This module contains concrete [ReportingStep] implementations + use std::collections::HashMap; use std::fmt::Display; use chrono::Datelike; -use crate::reporting::types::{ - BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId, -}; +use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; use crate::util::sofy_from_eofy; use super::calculator::ReportingGraphDependencies; use super::executor::ReportingExecutionError; use super::types::{ - DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, - ReportingStepArgs, ReportingStepId, VoidArgs, + BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind, + ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs, }; /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module @@ -96,6 +96,64 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { args: self.args.clone(), } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get all dependencies + let step_dependencies = dependencies.dependencies_for_step(&self.id()); + + // Identify the product_kinds dependency most recently generated + if self.product_kinds.len() != 1 { + panic!("AllTransactionsExceptRetainedEarnings.product_kinds.len() != 1"); + } + let product_kind = self.product_kinds[0]; + + for (product_id, product) in products.map().iter().rev() { + if step_dependencies.iter().any(|d| d.product == *product_id) { + // Store the result + products.insert( + ReportingProductId { + name: self.id().name, + kind: product_kind, + args: self.args.clone(), + }, + product.clone(), + ); + + return Ok(()); + } + } + + // No dependencies?! - store empty result + let product: Box = match self.product_kinds[0] { + ReportingProductKind::Transactions => Box::new(Transactions { + transactions: Vec::new(), + }), + ReportingProductKind::BalancesAt => Box::new(BalancesAt { + balances: HashMap::new(), + }), + ReportingProductKind::BalancesBetween => Box::new(BalancesBetween { + balances: HashMap::new(), + }), + ReportingProductKind::Generic => panic!("Requested AllTransactionsExceptRetainedEarnings.Generic but no available dependencies to provide it"), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: product_kind, + args: self.args.clone(), + }, + product, + ); + + Ok(()) + } } #[derive(Debug)] @@ -155,6 +213,62 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get opening balances from AllTransactionsExceptRetainedEarnings + let opening_balances = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + + // Get RetainedEarningsToEquity transactions + let transactions = products + .get_or_err(&ReportingProductId { + name: "RetainedEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + + // Update balances + let mut balances = BalancesAt { + balances: opening_balances.balances.clone(), + }; + + for transaction in transactions.transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } #[derive(Debug)] @@ -226,6 +340,31 @@ impl ReportingStep for CalculateIncomeTax { } } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: CalculateIncomeTax.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + Box::new(transactions), + ); + + Ok(()) + } } #[derive(Debug)] @@ -285,6 +424,44 @@ impl ReportingStep for CombineOrdinaryTransactions { }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Sum balances of all dependencies + + let mut balances = BalancesAt { + balances: HashMap::new(), + }; + + for dependency in dependencies.dependencies_for_step(&self.id()) { + let dependency_balances = &products + .get_or_err(&dependency.product)? + .downcast_ref::() + .unwrap() + .balances; + for (account, balance) in dependency_balances.iter() { + let running_balance = balances.balances.get(account).unwrap_or(&0) + balance; + balances.balances.insert(account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } #[derive(Debug)] @@ -331,6 +508,8 @@ impl ReportingStep for DBBalances { fn execute( &self, _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { eprintln!("Stub: DBBalances.execute"); @@ -345,7 +524,7 @@ impl ReportingStep for DBBalances { kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, - ReportingProduct::BalancesAt(balances), + Box::new(balances), ); Ok(()) @@ -392,6 +571,31 @@ impl ReportingStep for PostUnreconciledStatementLines { args: Box::new(self.args.clone()), } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: PostUnreconciledStatementLines.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } } #[derive(Debug)] @@ -448,4 +652,29 @@ impl ReportingStep for RetainedEarningsToEquity { }), }] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: RetainedEarningsToEquity.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } } diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 7272eb3..99a3494 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -24,7 +24,9 @@ use downcast_rs::Downcast; use dyn_clone::DynClone; use dyn_eq::DynEq; use dyn_hash::DynHash; +use indexmap::IndexMap; +use crate::transaction::TransactionWithPostings; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; @@ -121,7 +123,7 @@ pub struct ReportingStepDynamicBuilder { // REPORTING PRODUCTS /// Identifies a [ReportingProduct] -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct ReportingProductId { pub name: &'static str, pub kind: ReportingProductKind, @@ -144,33 +146,84 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] -#[derive(Debug)] -pub enum ReportingProduct { - Transactions(Transactions), - BalancesAt(BalancesAt), - BalancesBetween(BalancesBetween), - Generic(Box), -} +pub trait ReportingProduct: Debug + Downcast + DynClone {} + +downcast_rs::impl_downcast!(ReportingProduct); +dyn_clone::clone_trait_object!(ReportingProduct); /// Records a list of transactions generated by a [ReportingStep] -#[derive(Debug)] -pub struct Transactions {} +#[derive(Clone, Debug)] +pub struct Transactions { + pub transactions: Vec, +} + +impl ReportingProduct for Transactions {} /// Records cumulative account balances at a particular point in time -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct BalancesAt { pub balances: HashMap, } +impl ReportingProduct for BalancesAt {} + /// Records the total value of transactions in each account between two points in time -#[derive(Debug)] -pub struct BalancesBetween {} +#[derive(Clone, Debug)] +pub struct BalancesBetween { + pub balances: HashMap, +} + +impl ReportingProduct for BalancesBetween {} /// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct: Debug {} +pub trait GenericReportingProduct: Debug + ReportingProduct {} -/// Convenience type mapping [ReportingProductId] to [ReportingProduct] -pub type ReportingProducts = HashMap; +/// Map from [ReportingProductId] to [ReportingProduct] +#[derive(Clone, Debug)] +pub struct ReportingProducts { + map: IndexMap>, +} + +impl ReportingProducts { + pub fn new() -> Self { + Self { + map: IndexMap::new(), + } + } + + pub fn map(&self) -> &IndexMap> { + &self.map + } + + pub fn insert(&mut self, key: ReportingProductId, value: Box) { + self.map.insert(key, value); + } + + pub fn get_or_err( + &self, + key: &ReportingProductId, + ) -> Result<&Box, ReportingExecutionError> { + match self.map.get(key) { + Some(value) => Ok(value), + None => Err(ReportingExecutionError::DependencyNotAvailable { + message: format!("Product {} not available when expected", key), + }), + } + } +} + +impl Display for ReportingProducts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "ReportingProducts {{\n{}\n}}", + self.map + .iter() + .map(|(k, v)| format!(" {}: {:?}", k, v)) + .collect::>() + .join(",\n") + )) + } +} // --------------- // REPORTING STEPS @@ -230,6 +283,8 @@ pub trait ReportingStep: Debug + Display + Downcast { fn execute( &self, context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { todo!("{}", self); diff --git a/src/transaction.rs b/src/transaction.rs index a17ebbc..5a4c1aa 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,25 +1,44 @@ /* DrCr: Web-based double-entry bookkeeping framework Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use chrono::NaiveDateTime; +use crate::QuantityInt; + +#[derive(Clone, Debug)] pub struct Transaction { pub id: Option, pub dt: NaiveDateTime, pub description: String, } + +#[derive(Clone, Debug)] +pub struct TransactionWithPostings { + pub transaction: Transaction, + pub postings: Vec, +} + +#[derive(Clone, Debug)] +pub struct Posting { + pub id: Option, + pub transaction_id: Option, + pub description: String, + pub account: String, + pub quantity: QuantityInt, + pub commodity: String, +} From 798c7d3c07ce00a8c970bbb1c48deb12970b51ed Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 21:53:35 +1000 Subject: [PATCH 15/45] Refactor update_balances_from_transactions --- src/reporting/builders.rs | 38 +++++--------------------------------- src/reporting/steps.rs | 13 ++----------- src/transaction.rs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 8cd3c11..54717ae 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -19,6 +19,8 @@ use std::collections::HashMap; use std::fmt::Display; +use crate::transaction::update_balances_from_transactions; + use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; use super::executor::ReportingExecutionError; use super::types::{ @@ -319,17 +321,7 @@ impl ReportingStep for GenerateBalances { let mut balances = BalancesAt { balances: HashMap::new(), }; - - for transaction in transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result products.insert( @@ -546,17 +538,7 @@ impl ReportingStep for UpdateBalancesAt { let mut balances = BalancesAt { balances: opening_balances_at.balances.clone(), }; - - for transaction in transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result products.insert( @@ -720,17 +702,7 @@ impl ReportingStep for UpdateBalancesBetween { let mut balances = BalancesBetween { balances: opening_balances.clone(), }; - - for transaction in transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result products.insert( diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index b541f4a..ed87930 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -24,6 +24,7 @@ use std::fmt::Display; use chrono::Datelike; use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; +use crate::transaction::update_balances_from_transactions; use crate::util::sofy_from_eofy; use super::calculator::ReportingGraphDependencies; @@ -245,17 +246,7 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { let mut balances = BalancesAt { balances: opening_balances.balances.clone(), }; - - for transaction in transactions.transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.transactions.iter()); // Store result products.insert( diff --git a/src/transaction.rs b/src/transaction.rs index 5a4c1aa..1220c95 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::collections::HashMap; + use chrono::NaiveDateTime; use crate::QuantityInt; @@ -42,3 +44,16 @@ pub struct Posting { pub quantity: QuantityInt, pub commodity: String, } + +pub(crate) fn update_balances_from_transactions<'a, I: Iterator>( + balances: &mut HashMap, + transactions: I, +) { + for transaction in transactions { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances.insert(posting.account.clone(), running_balance); + } + } +} From 4e945573700b11177cbcb3fe48f8ed6c90c537cd Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 22:26:40 +1000 Subject: [PATCH 16/45] Update documentation --- src/reporting/builders.rs | 8 ++++++-- src/reporting/calculator.rs | 8 ++++---- src/reporting/steps.rs | 17 +++++++++++++++++ src/util.rs | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 54717ae..a8f0027 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,6 +16,10 @@ along with this program. If not, see . */ +//! This module contains implementations of dynamic step builders +//! +//! See [ReportingContext::register_dynamic_builder][super::types::ReportingContext::register_dynamic_builder]. + use std::collections::HashMap; use std::fmt::Display; @@ -39,13 +43,13 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { BalancesAtToBalancesBetween::register_dynamic_builder(context); } +/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates #[derive(Debug)] pub struct BalancesAtToBalancesBetween { step_name: &'static str, args: DateStartDateEndArgs, } -/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween @@ -205,7 +209,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { } } -/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [super::steps::PostUnreconciledStatementLines]) +/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [PostUnreconciledStatementLines][super::steps::PostUnreconciledStatementLines]) #[derive(Debug)] pub struct GenerateBalances { step_name: &'static str, diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 862df18..0c70a98 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -23,7 +23,7 @@ use super::types::{ ReportingStepFromArgsFn, ReportingStepId, }; -/// List of dependencies between [ReportingStep]s and [ReportingProduct]s +/// List of dependencies between [ReportingStep]s and [ReportingProduct][super::types::ReportingProduct]s #[derive(Debug)] pub struct ReportingGraphDependencies { vec: Vec, @@ -35,7 +35,7 @@ impl ReportingGraphDependencies { &self.vec } - /// Record that the [ReportingStep] depends on the [ReportingProduct] + /// Record that the [ReportingStep] depends on the [ReportingProduct][super::types::ReportingProduct] pub fn add_dependency(&mut self, step: ReportingStepId, product: ReportingProductId) { if !self .vec @@ -52,7 +52,7 @@ impl ReportingGraphDependencies { } } -/// Represents that a [ReportingStep] depends on a [ReportingProduct] +/// Represents that a [ReportingStep] depends on a [ReportingProduct][super::types::ReportingProduct] #[derive(Debug)] pub struct Dependency { pub step: ReportingStepId, @@ -74,7 +74,7 @@ pub enum HasStepOrCanBuild<'a, 'b> { None, } -/// Determines whether the [ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder +/// Determines whether the [ReportingProduct][super::types::ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder pub fn has_step_or_can_build<'a, 'b>( product: &ReportingProductId, steps: &'a Vec>, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index ed87930..4d0d5f4 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -45,6 +45,11 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { RetainedEarningsToEquity::register_lookup_fn(context); } +/// Target representing all transactions except charging retained earnings to equity +/// +/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax]. +/// +/// Used as the basis for the income statement. #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { pub product_kinds: &'static [ReportingProductKind], // Must have single member @@ -157,6 +162,11 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { } } +/// Target representing all transactions including charging retained earnings to equity +/// +/// In other words, this is [AllTransactionsExceptRetainedEarnings] and [RetainedEarningsToEquity]. +/// +/// Used as the basis for the balance sheet. #[derive(Debug)] pub struct AllTransactionsIncludingRetainedEarnings { pub args: DateArgs, @@ -262,6 +272,7 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { } } +/// Calculates income tax #[derive(Debug)] pub struct CalculateIncomeTax {} @@ -358,6 +369,9 @@ impl ReportingStep for CalculateIncomeTax { } } +/// Combines all steps producing ordinary transactions +/// +/// By default, these are [DBBalances] and [PostUnreconciledStatementLines] #[derive(Debug)] pub struct CombineOrdinaryTransactions { pub args: DateArgs, @@ -455,6 +469,7 @@ impl ReportingStep for CombineOrdinaryTransactions { } } +/// Look up account balances from the database #[derive(Debug)] pub struct DBBalances { pub args: DateArgs, @@ -522,6 +537,7 @@ impl ReportingStep for DBBalances { } } +/// Generate transactions for unreconciled statement lines #[derive(Debug)] pub struct PostUnreconciledStatementLines { pub args: DateArgs, @@ -589,6 +605,7 @@ impl ReportingStep for PostUnreconciledStatementLines { } } +/// Transfer historical balances in income and expense accounts to the retained earnings equity account #[derive(Debug)] pub struct RetainedEarningsToEquity { pub args: DateArgs, diff --git a/src/util.rs b/src/util.rs index c8129c4..73bd8f9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,7 +18,7 @@ use chrono::{Datelike, NaiveDate}; +/// Return the start date of the financial year, given the end date of the financial year pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { - // Return the start date of the financial year, given the end date of the financial year return date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap(); } From 412b79ee458facde1e6a4835827cdf1cb3149335 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 22:29:18 +1000 Subject: [PATCH 17/45] Statically require single member for AllTransactionsExceptRetainedEarnings.product_kinds --- src/reporting/steps.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 4d0d5f4..29850bc 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -52,7 +52,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { /// Used as the basis for the income statement. #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub product_kinds: &'static [ReportingProductKind; 1], // Must have single member - represented as static array for compatibility with ReportingStepId pub args: Box, } @@ -78,7 +78,7 @@ impl AllTransactionsExceptRetainedEarnings { } fn from_args( - product_kinds: &'static [ReportingProductKind], + product_kinds: &'static [ReportingProductKind; 1], args: Box, ) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { @@ -113,10 +113,7 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { // Get all dependencies let step_dependencies = dependencies.dependencies_for_step(&self.id()); - // Identify the product_kinds dependency most recently generated - if self.product_kinds.len() != 1 { - panic!("AllTransactionsExceptRetainedEarnings.product_kinds.len() != 1"); - } + // Identify the product_kind dependency most recently generated let product_kind = self.product_kinds[0]; for (product_id, product) in products.map().iter().rev() { From 9fe7bf22a6bb3c2b1442b956ad1ec281d5986cb8 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Thu, 22 May 2025 00:25:51 +1000 Subject: [PATCH 18/45] Basic implementation of DBBalances --- Cargo.lock | 1654 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + drcr_testing.db | Bin 0 -> 1544192 bytes src/db.rs | 85 ++ src/lib.rs | 3 +- src/main.rs | 10 +- src/reporting/builders.rs | 4 +- src/reporting/steps.rs | 17 +- src/reporting/types.rs | 8 +- src/util.rs | 7 +- 10 files changed, 1776 insertions(+), 15 deletions(-) create mode 100644 drcr_testing.db create mode 100644 src/db.rs diff --git a/Cargo.lock b/Cargo.lock index 7a70601..202ed01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -17,18 +38,84 @@ dependencies = [ "libc", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.2.23" @@ -58,12 +145,116 @@ dependencies = [ "windows-link", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "2.0.1" @@ -88,17 +279,254 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] [[package]] name = "iana-time-zone" @@ -124,6 +552,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -134,6 +669,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -144,6 +685,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.172" @@ -159,7 +709,43 @@ dependencies = [ "dyn-clone", "dyn-eq", "dyn-hash", + "futures", "indexmap", + "sqlx", + "tokio", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", ] [[package]] @@ -168,6 +754,79 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -175,6 +834,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", ] [[package]] @@ -183,6 +852,107 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -201,18 +971,438 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -224,12 +1414,208 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -288,6 +1674,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "windows-core" version = "0.61.1" @@ -346,3 +1742,261 @@ checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index f6046dc..ff5cf66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,7 @@ downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" +futures = "0.3.31" indexmap = "2.9.0" +sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] } +tokio = { version = "1.45.0", features = ["full"] } diff --git a/drcr_testing.db b/drcr_testing.db new file mode 100644 index 0000000000000000000000000000000000000000..949df13fa0472029587ef9e88018f43df37a30ee GIT binary patch literal 1544192 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