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)) + } +}