freneng 0.1.2

A useful, async-first file renaming library
Documentation
//! # Fren Core Library
//! 
//! This library provides the core renaming engine that can be used by any frontend
//! (CLI, GUI, or other applications). It handles pattern parsing, file operations,
//! validation, and history management.
//! 
//! All operations are async and non-blocking, making it suitable for GUI applications
//! where blocking operations would freeze the user interface.

pub mod fs_ops;
pub mod pattern;
pub mod history;
pub mod validation;
pub mod audit;

use std::path::PathBuf;
pub use crate::fs_ops::{FileRename, FrenError, RenameExecutionResult, find_matching_files, find_matching_files_recursive, perform_renames};
pub use crate::validation::{validate_renames, ValidationIssue, ValidationResult};
pub use crate::audit::{log_audit_entry, log_audit_from_result, read_audit_log, clear_audit_log, AuditEntry};
use crate::pattern::apply_rename_pattern;
use crate::history::{History, RenameAction};
use tokio::fs;

/// Result of generating a preview of renaming operations.
/// 
/// Contains the list of proposed renames, any warnings encountered during
/// pattern parsing, and whether any generated names would be empty.
pub struct EnginePreviewResult {
    /// List of file rename operations that would be performed
    pub renames: Vec<FileRename>,
    /// Warnings about unknown tokens or other non-critical issues
    pub warnings: Vec<String>,
    /// Whether any generated filename would be empty (blocks execution)
    pub has_empty_names: bool,
}

/// The main renaming engine that can be consumed by any frontend (CLI/GUI).
/// 
/// This struct provides the core functionality for:
/// - Generating previews of rename operations
/// - Validating rename operations
/// - Managing undo operations
pub struct RenamingEngine;

impl RenamingEngine {
    /// Generates a preview of renaming operations for the given files and pattern asynchronously.
    /// 
    /// This method parses the pattern, applies it to each file, and returns
    /// a preview of what would happen if the renames were executed. It does
    /// not perform any actual file system operations, but may access file metadata
    /// for date/time placeholders.
    /// 
    /// # Arguments
    /// 
    /// * `files` - List of file paths to be renamed
    /// * `pattern` - Renaming pattern (e.g., "%N_v2.%E", "%L%N.%E")
    /// 
    /// # Returns
    /// 
    /// * `Ok(EnginePreviewResult)` - Preview of rename operations with warnings
    /// * `Err(FrenError)` - If pattern parsing fails or files are invalid
    /// 
    /// # Examples
    /// 
    /// ```
    /// # tokio_test::block_on(async {
    /// use freneng::RenamingEngine;
    /// use std::path::PathBuf;
    /// 
    /// let engine = RenamingEngine;
    /// let files = vec![PathBuf::from("photo.jpg")];
    /// let result = engine.generate_preview(&files, "%L%N_v2.%E").await.unwrap();
    /// assert_eq!(result.renames[0].new_name, "photo_v2.jpg");
    /// # })
    /// ```
    pub async fn generate_preview(
        &self,
        files: &[PathBuf],
        pattern: &str,
    ) -> Result<EnginePreviewResult, FrenError> {
        let mut renames = Vec::new();
        let mut all_warnings = Vec::new();
        let mut has_empty_names = false;

        // Process files concurrently for better performance
        let preview_futures: Vec<_> = files.iter().enumerate().map(|(idx, file)| {
            let pattern = pattern.to_string();
            let file = file.clone();
            async move {
                let pattern_result = apply_rename_pattern(&file, &pattern, idx + 1).await
                    .map_err(|e| FrenError::PatternApplication(e.to_string()))?;
                Ok((file, pattern_result))
            }
        }).collect();

        let results: Vec<Result<_, FrenError>> = futures::future::join_all(preview_futures).await;
        
        for result in results {
            let (file, pattern_result) = result?;
            let new_name = pattern_result.name;
            
            for warning in pattern_result.warnings {
                if !all_warnings.contains(&warning) {
                    all_warnings.push(warning);
                }
            }

            if new_name.trim().is_empty() {
                has_empty_names = true;
            }

            let parent = file.parent().ok_or_else(|| FrenError::Pattern("File has no parent directory".into()))?;
            let new_path = parent.join(&new_name);
            
            renames.push(FileRename {
                old_path: file,
                new_path,
                new_name,
            });
            
        }

        Ok(EnginePreviewResult {
            renames,
            warnings: all_warnings,
            has_empty_names,
        })
    }

    /// Checks if an undo operation is safe to perform asynchronously.
    /// 
    /// This method examines the history and determines which rename operations
    /// can be safely undone. It checks for conflicts such as:
    /// - Files that no longer exist at their renamed location
    /// - Original locations that are now occupied by other files
    /// 
    /// # Arguments
    /// 
    /// * `history` - The history containing rename actions to check
    /// 
    /// # Returns
    /// 
    /// A tuple containing:
    /// - `Vec<RenameAction>` - Actions that can be safely undone
    /// - `Vec<String>` - Conflict messages for actions that cannot be undone
    /// 
    /// # Examples
    /// 
    /// ```
    /// # tokio_test::block_on(async {
    /// use freneng::RenamingEngine;
    /// use freneng::history::{History, RenameAction};
    /// use std::path::PathBuf;
    /// 
    /// let engine = RenamingEngine;
    /// let history = History {
    ///     timestamp: chrono::Local::now(),
    ///     actions: vec![RenameAction {
    ///         old_path: PathBuf::from("old.txt"),
    ///         new_path: PathBuf::from("new.txt"),
    ///     }],
    /// };
    /// let (safe, conflicts) = engine.check_undo(&history).await;
    /// # })
    /// ```
    pub async fn check_undo(&self, history: &History) -> (Vec<RenameAction>, Vec<String>) {
        let mut safe_actions: Vec<RenameAction> = Vec::new();
        let mut conflicts = Vec::new();

        // Check all actions concurrently
        let check_futures: Vec<_> = history.actions.iter().map(|action| {
            let new_path = action.new_path.clone();
            let old_path = action.old_path.clone();
            async move {
                let new_name = new_path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
                let old_name = old_path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
                
                let new_exists = fs::metadata(&new_path).await.is_ok();
                let old_exists = fs::metadata(&old_path).await.is_ok();
                
                let mut has_conflict = false;
                let mut conflict_msg = None;
                
                if !new_exists {
                    conflict_msg = Some(format!("File no longer exists: {}", new_name));
                    has_conflict = true;
                } else if old_exists && old_path != new_path {
                    conflict_msg = Some(format!("Original location occupied: {}", old_name));
                    has_conflict = true;
                }
                
                (action.clone(), has_conflict, conflict_msg)
            }
        }).collect();

        let results: Vec<_> = futures::future::join_all(check_futures).await;
        
        for (action, has_conflict, conflict_msg) in results {
            if has_conflict {
                if let Some(msg) = conflict_msg {
                    conflicts.push(msg);
                }
            } else {
                safe_actions.push(action);
            }
        }
        
        (safe_actions, conflicts)
    }

    /// Performs the actual undo operation by reversing the given rename actions asynchronously.
    /// 
    /// This method renames files back from their new location to their original
    /// location. It should only be called with actions that have been verified
    /// as safe by `check_undo`. All operations are non-blocking.
    /// 
    /// # Arguments
    /// 
    /// * `actions` - List of rename actions to undo (should be pre-validated)
    /// 
    /// # Returns
    /// 
    /// * `Ok(usize)` - Number of files successfully undone
    /// * `Err(FrenError)` - If any rename operation fails
    /// 
    /// # Examples
    /// 
    /// ```no_run
    /// # tokio_test::block_on(async {
    /// use freneng::RenamingEngine;
    /// use freneng::history::RenameAction;
    /// use std::path::PathBuf;
    /// 
    /// let engine = RenamingEngine;
    /// let actions = vec![RenameAction {
    ///     old_path: PathBuf::from("old.txt"),
    ///     new_path: PathBuf::from("new.txt"),
    /// }];
    /// let count = engine.apply_undo(actions).await.unwrap();
    /// # })
    /// ```
    pub async fn apply_undo(&self, actions: Vec<RenameAction>) -> Result<usize, FrenError> {
        // Perform all undo operations concurrently
        let undo_futures: Vec<_> = actions.into_iter().map(|action| {
            let new_path = action.new_path.clone();
            let old_path = action.old_path.clone();
            async move {
                fs::rename(&new_path, &old_path).await?;
                Ok::<(), FrenError>(())
            }
        }).collect();

        let results: Vec<Result<_, FrenError>> = futures::future::join_all(undo_futures).await;
        
        let mut count = 0;
        for result in results {
            result?;
            count += 1;
        }
        
        Ok(count)
    }
    
    /// Validates a set of renames before execution asynchronously.
    /// 
    /// This method performs comprehensive validation including:
    /// - Filename validity (characters, length, format)
    /// - File system checks (existence, permissions)
    /// - Circular rename detection
    /// - Reserved filename checks (OS-specific)
    /// 
    /// All file system operations are non-blocking.
    /// 
    /// # Arguments
    /// 
    /// * `renames` - List of proposed rename operations
    /// * `overwrite` - Whether to allow overwriting existing files
    /// 
    /// # Returns
    /// 
    /// A `ValidationResult` containing:
    /// - Valid renames that can proceed
    /// - Issues found with specific files
    /// 
    /// # Examples
    /// 
    /// ```
    /// # tokio_test::block_on(async {
    /// use freneng::RenamingEngine;
    /// use freneng::FileRename;
    /// use std::path::PathBuf;
    /// 
    /// let engine = RenamingEngine;
    /// let renames = vec![FileRename {
    ///     old_path: PathBuf::from("old.txt"),
    ///     new_path: PathBuf::from("new.txt"),
    ///     new_name: "new.txt".to_string(),
    /// }];
    /// let result = engine.validate(&renames, false).await;
    /// # })
    /// ```
    pub async fn validate(&self, renames: &[FileRename], overwrite: bool) -> ValidationResult {
        validate_renames(renames, overwrite).await
    }
}