naru-config 0.7.0

A security-first configuration manager with encryption and audit logging
Documentation
use crate::core::constants;
use crate::core::models::{
    ConfigFile, ConfigValueEntry, EnvironmentConfig, FieldDefinition, SchemaFile, ValidationRules,
};
use crate::core::persistence;
use crate::core::persistence::PersistenceError;
use anyhow::Result;
use console::style;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Template {
    pub name: String,
    pub version: String,
    pub description: String,
    pub variables: Vec<TemplateVariable>,
    pub schema_fields: Vec<FieldDefinition>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TemplateVariable {
    pub key: String,
    pub kind: String,
    pub default: Option<String>,
    pub note: Option<String>,
    pub secret: bool,
    pub rules: Option<ValidationRules>,
}

pub fn template_create(name: &str, include_secrets: bool) -> Result<(), PersistenceError> {
    let config: ConfigFile = persistence::atomic_read_config(|c| c.clone())?;
    let schema: SchemaFile =
        persistence::atomic_read_json(constants::SCHEMA_FILE, |s: &SchemaFile| s.clone())
            .unwrap_or(SchemaFile {
                version: "1.0".to_string(),
                fields: vec![],
            });

    let templates_dir = PathBuf::from(constants::NARU_DIR).join("templates");
    fs::create_dir_all(&templates_dir).map_err(|e| PersistenceError::IoError {
        source: std::io::Error::other(format!("Cannot create templates directory: {}", e)),
    })?;

    let mut variables = Vec::new();

    for env_config in config.environments.values() {
        for (key, entry) in &env_config.entries {
            if !include_secrets && entry.is_secret {
                continue;
            }

            if !variables.iter().any(|v: &TemplateVariable| v.key == *key) {
                let rules = schema
                    .fields
                    .iter()
                    .find(|f| f.key == *key)
                    .and_then(|f| f.validation.clone());

                variables.push(TemplateVariable {
                    key: key.clone(),
                    kind: entry.r#type.clone(),
                    default: if entry.is_secret {
                        None
                    } else {
                        Some(entry.value.clone())
                    },
                    note: schema
                        .fields
                        .iter()
                        .find(|f| f.key == *key)
                        .and_then(|f| f.description.clone()),
                    secret: entry.is_secret,
                    rules,
                });
            }
        }
    }

    let template = Template {
        name: name.to_string(),
        version: config.version.clone(),
        description: format!("Template created from {}", config.project_name),
        variables,
        schema_fields: schema.fields,
    };

    let template_path = templates_dir.join(format!("{}.toml", name));
    let toml_str = toml::to_string_pretty(&template).map_err(|e| PersistenceError::IoError {
        source: std::io::Error::other(format!("Cannot serialize template: {}", e)),
    })?;

    fs::write(&template_path, toml_str).map_err(|e| PersistenceError::IoError {
        source: std::io::Error::other(format!("Cannot write template: {}", e)),
    })?;

    println!(
        "{} Template '{}' created with {} variables",
        style("").green(),
        style(name).cyan(),
        style(template.variables.len()).yellow()
    );

    Ok(())
}

pub fn template_apply(name: &str, env: &str) -> Result<(), PersistenceError> {
    let templates_dir = PathBuf::from(constants::NARU_DIR).join("templates");
    let template_path = templates_dir.join(format!("{}.toml", name));

    if !template_path.exists() {
        return Err(PersistenceError::IoError {
            source: std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("Template '{}' not found", name),
            ),
        });
    }

    let template_content =
        fs::read_to_string(&template_path).map_err(|e| PersistenceError::IoError {
            source: std::io::Error::other(format!("Cannot read template: {}", e)),
        })?;

    let template: Template =
        toml::from_str(&template_content).map_err(|e| PersistenceError::IoError {
            source: std::io::Error::other(format!("Invalid template format: {}", e)),
        })?;

    let mut applied_count = 0;

    persistence::atomic_update_config(|config| {
        if !config.environments.contains_key(env) {
            config.environments.insert(
                env.to_string(),
                EnvironmentConfig {
                    parent: None,
                    entries: HashMap::new(),
                },
            );
        }

        if let Some(env_config) = config.environments.get_mut(env) {
            for var in &template.variables {
                if !env_config.entries.contains_key(&var.key) {
                    let value = var.default.clone().unwrap_or_else(|| {
                        if var.secret {
                            String::new()
                        } else {
                            format!("TODO_{}", var.key)
                        }
                    });

                    env_config.entries.insert(
                        var.key.clone(),
                        ConfigValueEntry {
                            value,
                            r#type: var.kind.clone(),
                            is_secret: var.secret,
                            encrypted: false,
                        },
                    );
                    applied_count += 1;
                }
            }
        }
        Ok(())
    })?;

    if !template.schema_fields.is_empty() {
        persistence::atomic_update_json(
            constants::SCHEMA_FILE,
            |current_schema: &mut SchemaFile| {
                for field in &template.schema_fields {
                    if !current_schema.fields.iter().any(|f| f.key == field.key) {
                        current_schema.fields.push(field.clone());
                    }
                }
                Ok(())
            },
        )?;
    }

    println!(
        "{} Template '{}' applied: {} variables added to '{}'",
        style("").green(),
        style(name).cyan(),
        style(applied_count).yellow(),
        style(env).cyan()
    );

    Ok(())
}

pub fn template_list() -> Result<(), PersistenceError> {
    let templates_dir = PathBuf::from(constants::NARU_DIR).join("templates");

    if !templates_dir.exists() {
        println!("{} No templates directory found", style("").blue());
        return Ok(());
    }

    let mut templates = Vec::new();

    for entry in fs::read_dir(&templates_dir).map_err(|e| PersistenceError::IoError {
        source: std::io::Error::other(format!("Cannot read templates directory: {}", e)),
    })? {
        let entry = entry.map_err(|e| PersistenceError::IoError {
            source: std::io::Error::other(format!("Error reading directory: {}", e)),
        })?;

        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) == Some("toml") {
            if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
                templates.push(name.to_string());
            }
        }
    }

    if templates.is_empty() {
        println!("{} No templates found", style("").blue());
    } else {
        println!("{} Available templates:", style("").green());
        for template in templates {
            println!("{}", style(&template).cyan());
        }
    }

    Ok(())
}