maskit 0.1.1

A CLI tool to mask sensitive fields in configuration files (JSON/YAML/TOML)
Documentation
pub mod config;
pub mod error;

use colored::*;
use std::fs;
use std::path::PathBuf;

use crate::config::{mask_config, ConfigFormat};
use crate::error::{MaskitError, Result};

pub struct MaskitProcessor {
    input_path: PathBuf,
    output_path: Option<PathBuf>,
    keywords: Vec<String>,
}

impl MaskitProcessor {
    /// Create a new MaskitProcessor
    pub fn new(input_path: PathBuf, output_path: Option<PathBuf>, keywords: Vec<String>) -> Self {
        Self {
            input_path,
            output_path,
            keywords,
        }
    }

    /// Generate the default output path by adding .maskit before the extension
    fn generate_output_path(&self) -> Result<PathBuf> {
        let file_name = self
            .input_path
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or_else(|| MaskitError::InvalidPath("Invalid input file name".to_string()))?;

        let extension = self
            .input_path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("");

        let base_name = if !extension.is_empty() && file_name.ends_with(&format!(".{}", extension))
        {
            &file_name[..file_name.len() - extension.len() - 1]
        } else {
            file_name
        };

        let new_name = if extension.is_empty() {
            format!("{}.maskit", base_name)
        } else {
            format!("{}.maskit.{}", base_name, extension)
        };

        Ok(PathBuf::from(new_name))
    }

    /// Process the configuration file
    pub fn process(&self, silent: bool) -> Result<()> {
        // Print processing header
        if !silent {
            println!("{}", "🔒 Maskit - Configuration Masking Tool".cyan().bold());
        }

        // Validate input file exists
        if !self.input_path.exists() {
            return Err(MaskitError::FileRead(format!(
                "File not found: {}",
                self.input_path.display()
            )));
        }

        // Detect format
        let format = ConfigFormat::from_path(&self.input_path)?;
        if !silent {
            println!("{} {}", "📄 Input:".green(), self.input_path.display());
            println!("{} {:?}", "📋 Format:".green(), format);
        }

        // Read input file
        let content = fs::read_to_string(&self.input_path)
            .map_err(|e| MaskitError::FileRead(e.to_string()))?;

        // Process the content
        if !silent {
            println!("{} Masking sensitive fields...", "🔍".yellow());
            println!("{} {:?}", "📝 Keywords:".green(), self.keywords);
        }

        let masked_content = mask_config(&content, format, &self.keywords)?;

        // Determine output path
        let output_path = match &self.output_path {
            Some(path) => path.clone(),
            None => self.generate_output_path()?,
        };

        // Write output file
        fs::write(&output_path, masked_content)
            .map_err(|e| MaskitError::FileWrite(e.to_string()))?;

        if !silent {
            println!("{} {}", "✅ Output:".green().bold(), output_path.display());
            println!("{}", "🎉 Masking completed successfully!".green().bold());
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_generate_output_path() {
        let processor =
            MaskitProcessor::new(PathBuf::from("config.json"), None, vec!["key".to_string()]);

        let output_path = processor.generate_output_path().unwrap();
        assert_eq!(output_path.to_str().unwrap(), "config.maskit.json");
    }

    #[test]
    fn test_generate_output_path_complex() {
        let processor = MaskitProcessor::new(
            PathBuf::from("/path/to/my.config.yaml"),
            None,
            vec!["key".to_string()],
        );

        let output_path = processor.generate_output_path().unwrap();
        assert_eq!(output_path.to_str().unwrap(), "my.config.maskit.yaml");
    }

    #[test]
    fn test_process_json_file() {
        let temp_dir = TempDir::new().unwrap();
        let input_path = temp_dir.path().join("test.json");

        let json_content = r#"{
            "api_key": "secret123",
            "base_url": "http://example.com"
        }"#;

        fs::write(&input_path, json_content).unwrap();

        // Change to temp directory to test output in current dir
        let original_dir = std::env::current_dir().unwrap();
        std::env::set_current_dir(&temp_dir).unwrap();

        let processor = MaskitProcessor::new(input_path.clone(), None, vec!["key".to_string()]);

        processor.process(false).unwrap();

        // Output should be in current directory (temp_dir)
        let output_path = temp_dir.path().join("test.maskit.json");
        assert!(output_path.exists());

        let masked_content = fs::read_to_string(output_path).unwrap();
        assert!(masked_content.contains("*****"));
        assert!(masked_content.contains("http://example.com"));

        // Restore original directory
        std::env::set_current_dir(original_dir).unwrap();
    }
}