paladin-ai 0.5.0

Enterprise AI orchestration framework with multi-agent coordination patterns
Documentation
//! .env file template generation

use crate::application::cli::error::CliError;
use std::collections::HashMap;

/// Template for generating .env files
pub struct EnvTemplate;

impl EnvTemplate {
    /// Create a new EnvTemplate
    pub fn new() -> Self {
        Self
    }

    /// Generate .env file content from API keys
    ///
    /// # Arguments
    /// * `api_keys` - Map of provider enum keys to API key values
    /// * `existing_content` - Optional existing .env content for merging
    pub fn generate<K>(
        &self,
        api_keys: &HashMap<K, String>,
        existing_content: Option<&str>,
    ) -> Result<String, CliError>
    where
        K: std::fmt::Debug + std::hash::Hash + Eq,
    {
        let mut content = String::new();

        // Header comment
        content.push_str("# Paladin Environment Configuration\n");
        content.push_str("# Generated by 'paladin onboarding'\n\n");

        // LLM Provider section
        content.push_str("# LLM Provider Configuration\n");

        // Extract API keys as strings (using Debug format as fallback)
        for (key, value) in api_keys {
            let key_name = format!("{:?}", key).to_uppercase();
            let env_var = if key_name.contains("OPENAI") {
                "OPENAI_API_KEY"
            } else if key_name.contains("ANTHROPIC") {
                "ANTHROPIC_API_KEY"
            } else if key_name.contains("DEEPSEEK") {
                "DEEPSEEK_API_KEY"
            } else {
                continue;
            };

            content.push_str(&format!("{}={}\n", env_var, value));
        }

        content.push('\n');

        // Optional Services section
        content.push_str("# Optional Services (uncomment to use)\n");
        content.push_str("# REDIS_URL=redis://localhost:6379\n");
        content.push_str("# QDRANT_URL=http://localhost:6333\n");
        content.push_str("# MINIO_ENDPOINT=localhost:9000\n");
        content.push_str("# MINIO_ACCESS_KEY=minioadmin\n");
        content.push_str("# MINIO_SECRET_KEY=minioadmin\n");

        // Merge with existing content if provided
        if let Some(existing) = existing_content {
            content = self.merge_env_content(&content, existing)?;
        }

        Ok(content)
    }

    /// Merge new content with existing .env file content
    ///
    /// Intelligent merging that:
    /// - Preserves existing keys not in new content
    /// - Updates keys that exist in both
    /// - Adds new keys from new content
    fn merge_env_content(&self, new_content: &str, existing: &str) -> Result<String, CliError> {
        let mut merged = HashMap::new();
        let mut comments = Vec::new();

        // Parse existing content
        for line in existing.lines() {
            if line.trim().starts_with('#') || line.trim().is_empty() {
                comments.push(line.to_string());
            } else if let Some((key, value)) = line.split_once('=') {
                merged.insert(key.trim().to_string(), value.trim().to_string());
            }
        }

        // Parse new content and override/add keys
        for line in new_content.lines() {
            if line.trim().starts_with('#') || line.trim().is_empty() {
                // Skip comments from new content during merge
                continue;
            } else if let Some((key, value)) = line.split_once('=') {
                // New keys override existing
                merged.insert(key.trim().to_string(), value.trim().to_string());
            }
        }

        // Build merged content
        let mut result = String::new();
        result.push_str("# Paladin Environment Configuration\n");
        result.push_str("# Merged by 'paladin onboarding'\n\n");

        // Add LLM provider keys
        result.push_str("# LLM Provider Configuration\n");
        for key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "DEEPSEEK_API_KEY"] {
            if let Some(value) = merged.get(key) {
                result.push_str(&format!("{}={}\n", key, value));
            }
        }

        result.push('\n');

        // Add other keys
        result.push_str("# Other Configuration\n");
        for (key, value) in &merged {
            if !key.ends_with("_API_KEY") {
                result.push_str(&format!("{}={}\n", key, value));
            }
        }

        Ok(result)
    }
}

impl Default for EnvTemplate {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_generate_env_basic() {
        let template = EnvTemplate::new();
        let mut keys = HashMap::new();
        keys.insert("OpenAI", "sk-test123".to_string());

        let result = template.generate(&keys, None).unwrap();

        assert!(result.contains("OPENAI_API_KEY=sk-test123"));
        assert!(result.contains("# LLM Provider Configuration"));
        assert!(result.contains("# Optional Services"));
    }

    #[test]
    fn test_merge_env_content() {
        let template = EnvTemplate::new();

        let existing = "OPENAI_API_KEY=old-key\nCUSTOM_VAR=value1\n";
        let new = "# New config\nOPENAI_API_KEY=new-key\nANTHROPIC_API_KEY=claude-key\n";

        let result = template.merge_env_content(new, existing).unwrap();

        assert!(result.contains("OPENAI_API_KEY=new-key")); // Updated
        assert!(result.contains("ANTHROPIC_API_KEY=claude-key")); // New
        assert!(result.contains("CUSTOM_VAR=value1")); // Preserved
        assert!(!result.contains("old-key")); // Replaced
    }
}