sandl 0.1.0

A framework for building parallel execution engines with dependency management, type-safe method dispatch, and event observation.
Documentation
use crate::{Error, Result, Value};
use std::{collections::HashMap, time::Duration};

#[derive(Debug)]
pub struct SliceResults {
    pub method_results: HashMap<(String, String), Result<Value>>,
    pub duration: Duration,
}

impl SliceResults {
    pub fn new() -> Self {
        Self {
            method_results: HashMap::new(),
            duration: Duration::ZERO,
        }
    }

    pub fn add_result(&mut self, layer: String, method: String, result: Result<Value>) {
        self.method_results.insert((layer, method), result);
    }

    pub fn set_duration(&mut self, duration: Duration) {
        self.duration = duration;
    }
}

pub type RunResults = HashMap<String, Result<SliceResults>>;

pub trait RunResultsExt {
    fn total_slices(&self) -> usize;
    fn successful_slices(&self) -> usize;
    fn failed_slices(&self) -> usize;

    fn total_methods(&self) -> usize;
    fn successful_methods(&self) -> usize;
    fn failed_methods(&self) -> usize;

    fn is_all_success(&self) -> bool;
    fn has_failures(&self) -> bool;
    fn summary(&self) -> String;

    fn get_slice_errors(&self) -> Vec<(&String, &Error)>;
    fn get_all_method_errors(&self) -> Vec<(&String, &String, &String, &Error)>;
    fn get_execution_errors(&self) -> Vec<(&String, &String, &String, &Error)>;

    fn from_slice(&self, slice_name: &str) -> Option<&Result<SliceResults>>;
    fn slice_names(&self) -> Vec<&String>;

    fn average_slice_duration(&self) -> Option<Duration>;
    fn min_slice_duration(&self) -> Option<Duration>;
    fn max_slice_duration(&self) -> Option<Duration>;
    fn timing_summary(&self) -> String;
}

impl RunResultsExt for RunResults {
    fn total_slices(&self) -> usize {
        self.len()
    }

    fn successful_slices(&self) -> usize {
        self.values().filter(|result| result.is_ok()).count()
    }

    fn failed_slices(&self) -> usize {
        self.values().filter(|result| result.is_err()).count()
    }

    fn total_methods(&self) -> usize {
        self.values()
            .filter_map(|result| result.as_ref().ok())
            .map(|slice_results| slice_results.method_results.len())
            .sum()
    }

    fn successful_methods(&self) -> usize {
        self.values()
            .filter_map(|result| result.as_ref().ok())
            .map(|slice_results| {
                slice_results
                    .method_results
                    .values()
                    .filter(|result| result.is_ok())
                    .count()
            })
            .sum()
    }

    fn failed_methods(&self) -> usize {
        self.values()
            .filter_map(|result| result.as_ref().ok())
            .map(|slice_results| {
                slice_results
                    .method_results
                    .values()
                    .filter(|result| result.is_err())
                    .count()
            })
            .sum()
    }

    fn is_all_success(&self) -> bool {
        self.successful_slices() == self.total_slices()
            && self.successful_methods() == self.total_methods()
    }

    fn has_failures(&self) -> bool {
        self.failed_slices() > 0 || self.failed_methods() > 0
    }

    fn summary(&self) -> String {
        let total_slices = self.total_slices();
        let successful_slices = self.successful_slices();
        let total_methods = self.total_methods();
        let successful_methods = self.successful_methods();

        format!(
            "Slices: {}/{} succeeded, Methods: {}/{} succeeded",
            successful_slices, total_slices, successful_methods, total_methods
        )
    }

    fn get_slice_errors(&self) -> Vec<(&String, &Error)> {
        self.iter()
            .filter_map(|(slice_name, result)| result.as_ref().err().map(|e| (slice_name, e)))
            .collect()
    }

    fn get_all_method_errors(&self) -> Vec<(&String, &String, &String, &Error)> {
        let mut errors = Vec::new();

        for (slice_name, slice_result) in self {
            if let Ok(slice_results) = slice_result {
                for ((layer, method), method_result) in &slice_results.method_results {
                    if let Err(e) = method_result {
                        errors.push((slice_name, layer, method, e));
                    }
                }
            }
        }

        errors
    }

    fn from_slice(&self, slice_name: &str) -> Option<&Result<SliceResults>> {
        self.get(slice_name)
    }

    fn slice_names(&self) -> Vec<&String> {
        self.keys().collect()
    }

    fn get_execution_errors(&self) -> Vec<(&String, &String, &String, &Error)> {
        self.get_all_method_errors()
            .into_iter()
            .filter(|(_, _, _, error)| error.is_execution_error())
            .collect()
    }

    fn average_slice_duration(&self) -> Option<Duration> {
        let durations: Vec<Duration> = self
            .values()
            .filter_map(|result| result.as_ref().ok())
            .map(|slice_results| slice_results.duration)
            .collect();

        if durations.is_empty() {
            return None;
        }

        let total_nanos: u128 = durations.iter().map(|d| d.as_nanos()).sum();
        let avg_nanos = total_nanos / durations.len() as u128;

        Some(Duration::from_nanos(avg_nanos as u64))
    }

    fn min_slice_duration(&self) -> Option<Duration> {
        self.values()
            .filter_map(|result| result.as_ref().ok())
            .map(|slice_results| slice_results.duration)
            .min()
    }

    fn max_slice_duration(&self) -> Option<Duration> {
        self.values()
            .filter_map(|result| result.as_ref().ok())
            .map(|slice_results| slice_results.duration)
            .max()
    }

    fn timing_summary(&self) -> String {
        let avg = self.average_slice_duration();
        let min = self.min_slice_duration();
        let max = self.max_slice_duration();

        format!(
            "Avg: {:?}, Min: {:?}, Max: {:?}",
            avg.unwrap_or(Duration::ZERO),
            min.unwrap_or(Duration::ZERO),
            max.unwrap_or(Duration::ZERO)
        )
    }
}