/* 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) }