runeforge 25.8.0

Blueprint to optimal stack JSON converter - Part of Rune* Ecosystem
use anyhow::Result;
use sha2::{Digest, Sha256};
use serde::Serialize;

pub struct PlanHasher;

impl PlanHasher {
    /// Generate a deterministic hash for a plan given the same input and seed
    pub fn hash_plan<T: Serialize>(plan: &T, seed: u64) -> Result<String> {
        let plan_json = serde_json::to_string(plan)?;
        let mut hasher = Sha256::new();
        
        // Include seed for determinism
        hasher.update(seed.to_be_bytes());
        hasher.update(plan_json.as_bytes());
        
        let result = hasher.finalize();
        Ok(hex::encode(result))
    }
    
    /// Generate a short hash (first 8 characters) for display purposes
    pub fn hash_plan_short<T: Serialize>(plan: &T, seed: u64) -> Result<String> {
        let full_hash = Self::hash_plan(plan, seed)?;
        Ok(full_hash[..8].to_string())
    }
}

/// Utility functions for random number generation with deterministic seeds
pub mod random {
    use rand::{rngs::StdRng, Rng, SeedableRng};
    
    pub fn create_rng(seed: u64) -> StdRng {
        StdRng::seed_from_u64(seed)
    }
    
    pub fn weighted_choice<'a, T>(items: &'a [T], weights: &[f64], rng: &mut StdRng) -> Option<&'a T> {
        if items.is_empty() || items.len() != weights.len() {
            return None;
        }
        
        let total_weight: f64 = weights.iter().sum();
        if total_weight <= 0.0 {
            return None;
        }
        
        let random_value = rng.gen::<f64>() * total_weight;
        let mut cumulative_weight = 0.0;
        
        for (i, &weight) in weights.iter().enumerate() {
            cumulative_weight += weight;
            if random_value <= cumulative_weight {
                return Some(&items[i]);
            }
        }
        
        // Fallback to last item (shouldn't happen with proper weights)
        items.last()
    }
}

/// Utility functions for cost calculations
pub mod cost {
    /// Calculate monthly cost based on usage patterns
    pub fn calculate_monthly_cost(base_cost: f64, rps: f64, multiplier: f64) -> f64 {
        base_cost + (rps * multiplier * 24.0 * 30.0 / 1_000_000.0) // Convert to monthly cost
    }
    
    /// Apply regional pricing adjustments
    pub fn apply_regional_pricing(base_cost: f64, region: &str) -> f64 {
        match region {
            "us-east-1" => base_cost,
            "us-west-2" => base_cost * 1.1,
            "eu-west-1" => base_cost * 1.2,
            "ap-southeast-1" => base_cost * 1.3,
            _ => base_cost,
        }
    }
}

/// Utility functions for SLO calculations
pub mod slo {
    /// Calculate composite SLO based on multiple service SLOs
    pub fn composite_availability(availabilities: &[f64]) -> f64 {
        availabilities.iter().product()
    }
    
    /// Calculate 95th percentile latency from multiple components
    pub fn composite_p95_latency(latencies: &[f64]) -> f64 {
        // Simple sum for sequential operations
        latencies.iter().sum()
    }
    
    /// Calculate parallel operations latency (max of concurrent operations)
    pub fn parallel_p95_latency(latencies: &[f64]) -> f64 {
        latencies.iter().fold(0.0, |acc, &x| acc.max(x))
    }
}

/// Configuration utilities
pub mod config {
    use std::path::Path;
    
    /// Check if a file exists and is readable
    pub fn file_exists_and_readable(path: &Path) -> bool {
        path.exists() && path.is_file()
    }
    
    /// Get file extension as string
    pub fn get_file_extension(path: &Path) -> Option<String> {
        path.extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.to_lowercase())
    }
    
    /// Validate file format based on extension
    pub fn is_supported_format(path: &Path) -> bool {
        match get_file_extension(path) {
            Some(ext) => matches!(ext.as_str(), "yaml" | "yml" | "json"),
            None => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_plan_hash_deterministic() {
        let plan = json!({
            "language": "Rust",
            "backend": "Actix Web"
        });
        
        let hash1 = PlanHasher::hash_plan(&plan, 42).unwrap();
        let hash2 = PlanHasher::hash_plan(&plan, 42).unwrap();
        
        assert_eq!(hash1, hash2);
    }
    
    #[test]
    fn test_plan_hash_different_seeds() {
        let plan = json!({
            "language": "Rust",
            "backend": "Actix Web"
        });
        
        let hash1 = PlanHasher::hash_plan(&plan, 42).unwrap();
        let hash2 = PlanHasher::hash_plan(&plan, 43).unwrap();
        
        assert_ne!(hash1, hash2);
    }
    
    #[test]
    fn test_short_hash() {
        let plan = json!({
            "language": "Rust"
        });
        
        let short_hash = PlanHasher::hash_plan_short(&plan, 42).unwrap();
        assert_eq!(short_hash.len(), 8);
    }
    
    #[test]
    fn test_weighted_choice() {
        let mut rng = random::create_rng(42);
        let items = vec!["A", "B", "C"];
        let weights = vec![0.1, 0.8, 0.1];
        
        let choice = random::weighted_choice(&items, &weights, &mut rng);
        assert!(choice.is_some());
    }
    
    #[test]
    fn test_cost_calculation() {
        let monthly_cost = cost::calculate_monthly_cost(10.0, 100.0, 0.001);
        assert!(monthly_cost > 10.0);
    }
    
    #[test]
    fn test_composite_availability() {
        let availabilities = vec![0.999, 0.998, 0.999];
        let composite = slo::composite_availability(&availabilities);
        assert!(composite < 0.999);
        assert!(composite > 0.995);
    }
    
    #[test]
    fn test_file_extension() {
        use std::path::PathBuf;
        
        let yaml_path = PathBuf::from("test.yaml");
        let json_path = PathBuf::from("test.json");
        let invalid_path = PathBuf::from("test.txt");
        
        assert!(config::is_supported_format(&yaml_path));
        assert!(config::is_supported_format(&json_path));
        assert!(!config::is_supported_format(&invalid_path));
    }
}