/*
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 .
*/
//! 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
}
/// 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 == product)
{
self.vec.push(Dependency { step, product });
}
}
/// 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();
}
}
/// Represents that a [ReportingStep] depends on a [ReportingProduct]
#[derive(Debug)]
pub struct Dependency {
pub step: ReportingStepId,
pub product: ReportingProductId,
}
/// Indicates an error during dependency resolution in [steps_for_targets]
#[derive(Debug)]
pub enum ReportingCalculationError {
UnknownStep { message: String },
NoStepForProduct { message: String },
CircularDependencies,
}
pub enum HasStepOrCanBuild<'a, 'b> {
HasStep(&'a Box),
CanLookup(ReportingStepFromArgsFn),
CanBuild(&'b ReportingStepDynamicBuilder),
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>,
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))
{
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
for builder in context.step_dynamic_builders.iter() {
if (builder.can_build)(
product.name,
product.kind,
&product.args,
steps,
dependencies,
context,
) {
return HasStepOrCanBuild::CanBuild(builder);
}
}
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 {
'check_each_dependency: for dependency in dependencies.vec.iter() {
if dependency.step == step.id() {
// 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
&& steps[*previous_step].id().args == dependency.product.args
&& steps[*previous_step]
.id()
.product_kinds
.contains(&dependency.product.kind)
{
continue 'check_each_dependency;
}
}
// Dependency is not met
return false;
}
}
true
}
/// 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> {
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();
for dependency in target.requires(&context) {
dependencies.add_dependency(target.id(), dependency);
}
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);
}
// 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.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.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.product.args) {
let new_step = from_args_fn(dependency.product.args.clone());
// Check new step meets the dependency
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.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.product.kind)
{
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);
continue;
}
}
// No explicit step for product - try builders
for builder in context.step_dynamic_builders.iter() {
if (builder.can_build)(
dependency.product.name,
dependency.product.kind,
&dependency.product.args,
&steps,
&dependencies,
&context,
) {
new_steps.push((builder.build)(
dependency.product.name,
dependency.product.kind,
dependency.product.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();
for dependency in new_step.requires(&context) {
dependencies.add_dependency(new_step.id(), dependency);
}
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);
}
}
// 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.product
),
});
}
if !steps.iter().any(|s| {
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.product, 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)
}