luff 0.2.1

Print files with formatting
Documentation
#![allow(unused_assignments)]
//! Error types for luff with rich diagnostic support
//!
//! All error types are re-exported at this module level for convenience.
//! Consumers should use `crate::error::ClipboardError`, `crate::error::ConfigError`, etc.

mod clipboard;
mod config;

pub use self::clipboard::ClipboardError;
pub use self::config::ConfigError;

#[cfg(feature = "cli")]
use miette::Diagnostic;
use std::path::PathBuf;
use thiserror::Error;

/// Result type alias for luff operations
pub type Result<T> = std::result::Result<T, Error>;

/// Main error type for luff with rich diagnostic support
#[derive(Error, Debug)]
#[cfg_attr(feature = "cli", derive(Diagnostic))]
pub enum Error {
    /// I/O error occurred during file system operations
    #[error("IO error: {0}")]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Check that the file exists and you have permission to access it"))
    )]
    Io(#[from] std::io::Error),

    /// Git command execution failed
    #[error("Git command failed: {command}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::git::command_failed)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Ensure git is installed and you're in a git repository"))
    )]
    GitCommandFailed {
        /// The git command that was executed
        command: String,
        /// Standard error output from the git command
        stderr: String,
        /// The underlying I/O error
        source: std::io::Error,
        /// Suggested resolution for the user
        suggestion: String,
    },

    /// Not in a git repository
    #[error("Not in a git repository: {message}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::git::not_in_repo)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Run this command from within a git repository or omit the --git flag"))
    )]
    NotInGitRepository {
        /// Error context message
        message: String,
        /// Git stderr output for debugging
        stderr: String,
        /// Suggested resolution for the user
        suggestion: String,
    },

    /// Git output was not valid UTF-8
    #[error("Invalid UTF-8 in git output")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::git::invalid_utf8)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Check that your repository paths contain valid UTF-8 characters"))
    )]
    GitInvalidUtf8 {
        /// UTF-8 conversion error details
        #[source]
        source: std::string::FromUtf8Error,
        /// Suggested resolution for the user
        suggestion: String,
    },

    /// Path validation failed
    #[error("Invalid path: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::path::invalid)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help(
            "Paths must be valid UTF-8, not contain null bytes, and be under 4096 characters"
        ))
    )]
    InvalidPath {
        /// The invalid path that was rejected
        path: PathBuf,
    },

    /// Requested file does not exist
    #[error("File not found: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::fs::not_found)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Check the path and ensure the file exists"))
    )]
    FileNotFound {
        /// Path to the missing file
        path: PathBuf,
    },

    /// Path exists but is not a regular file
    #[error("Not a file: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::fs::not_a_file)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help(
            "The path exists but is not a regular file. Use directory walk mode for directories."
        ))
    )]
    NotAFile {
        /// Path that is not a file
        path: PathBuf,
    },

    /// Path exists but is not a regular file (before opening)
    #[error("Not a regular file (refusing to open): {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::fs::not_regular_file)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help(
            "Only regular files can be processed. Directories and special files are skipped."
        ))
    )]
    NotARegularFile {
        /// Path that is not a regular file
        path: PathBuf,
    },

    /// Configuration validation or creation failed
    #[error("Configuration error: {message}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid)))]
    Config {
        /// Description of the configuration error
        message: String,
    },

    /// Configuration file error with diagnostic context
    #[error("Configuration file error: {0}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::file_error)))]
    ConfigFile(#[from] ConfigError),

    /// Directory walking or file iteration error
    #[error("Walker error: {message}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::walker::error)))]
    Walker {
        /// Description of the walker error
        message: String,
    },

    /// All specified files were invalid
    #[error("All {count} specified file(s) were invalid")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::fs::all_files_invalid)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Check that all file paths are valid and accessible"))
    )]
    AllFilesInvalid {
        /// Number of invalid files
        count: usize,
    },

    /// Output formatting or printing error
    #[error("Printer error: {message}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::printer::error)))]
    Printer {
        /// Description of the printer error
        message: String,
    },

    /// Clipboard operation failed
    #[error("Clipboard error: {0}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::clipboard::error)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help(
            "Check that a clipboard is available (requires display server on Linux)"
        ))
    )]
    Clipboard(#[from] ClipboardError),

    /// File too large to process
    #[error("File too large: {path} ({size} bytes exceeds {max_size} bytes)")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::fs::file_too_large)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Process smaller files or increase MAX_FILE_SIZE in the configuration"))
    )]
    FileTooLarge {
        /// Path to the oversized file
        path: PathBuf,
        /// Actual size of the file
        size: u64,
        /// Maximum allowed size
        max_size: u64,
    },

    /// File is not valid UTF-8 text
    #[error("File is not valid UTF-8 text: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::fs::invalid_utf8)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Only UTF-8 text files can be processed. Binary files are skipped."))
    )]
    InvalidUtf8 {
        /// Path to the invalid file
        path: PathBuf,
    },
}

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

    #[test]
    fn test_error_display() {
        let err = Error::NotInGitRepository {
            message: "test error".to_string(),
            stderr: "git error output".to_string(),
            suggestion: "run git init".to_string(),
        };
        assert!(err.to_string().contains("Not in a git repository"));
        assert!(err.to_string().contains("test error"));
    }

    #[test]
    fn test_file_not_found() {
        let path = PathBuf::from("/nonexistent");
        let err = Error::FileNotFound { path };
        assert!(err.to_string().contains("/nonexistent"));
    }

    #[test]
    fn test_error_from_config_error() {
        let config_err = ConfigError::InvalidPattern {
            pattern: "*.png".to_string(),
            pattern_type: "extension".to_string(),
            help: "No glob characters allowed".to_string(),
        };
        let err = Error::ConfigFile(config_err);
        assert!(err.to_string().contains("Invalid extension pattern"));
    }

    #[test]
    fn test_error_from_clipboard_error() {
        let clip_err = ClipboardError::Timeout;
        let err = Error::from(clip_err);
        assert!(err.to_string().contains("timed out"));
    }

    #[test]
    fn test_clipboard_error_preserves_structure() {
        let clip_err = ClipboardError::SizeExceeded {
            content_len: 2_000_000,
            limit: 1_000_000,
        };
        let err = Error::from(clip_err);
        match &err {
            Error::Clipboard(inner) => {
                assert!(matches!(
                    inner,
                    ClipboardError::SizeExceeded {
                        content_len: 2_000_000,
                        limit: 1_000_000,
                    }
                ));
            }
            _ => panic!("Expected Error::Clipboard, got {err:?}"),
        }
    }

    #[test]
    fn test_all_files_invalid_error() {
        let err = Error::AllFilesInvalid { count: 5 };
        assert!(
            err.to_string()
                .contains("All 5 specified file(s) were invalid")
        );
    }

    #[test]
    fn test_git_command_failed_fields() {
        let err = Error::GitCommandFailed {
            command: "git rev-parse".to_string(),
            stderr: "fatal: not a git repository".to_string(),
            source: std::io::Error::other("git not found"),
            suggestion: "install git".to_string(),
        };

        match err {
            Error::GitCommandFailed {
                command,
                stderr,
                source: _,
                suggestion,
            } => {
                assert_eq!(command, "git rev-parse");
                assert!(stderr.contains("not a git repository"));
                assert_eq!(suggestion, "install git");
            }
            _ => panic!("Unexpected error variant"),
        }
    }

    #[test]
    fn test_not_in_git_repository_fields() {
        let err = Error::NotInGitRepository {
            message: "no git repo".to_string(),
            stderr: "fatal: not a git repository".to_string(),
            suggestion: "run git init".to_string(),
        };

        match err {
            Error::NotInGitRepository {
                message,
                stderr,
                suggestion,
            } => {
                assert_eq!(message, "no git repo");
                assert!(stderr.contains("not a git repository"));
                assert_eq!(suggestion, "run git init");
            }
            _ => panic!("Unexpected error variant"),
        }
    }

    #[test]
    fn test_git_invalid_utf8_fields() {
        let utf8_error = String::from_utf8(vec![0xff]).unwrap_err();
        let err = Error::GitInvalidUtf8 {
            source: utf8_error,
            suggestion: "check git output encoding".to_string(),
        };

        match err {
            Error::GitInvalidUtf8 {
                source: _,
                suggestion,
            } => {
                assert_eq!(suggestion, "check git output encoding");
            }
            _ => panic!("Unexpected error variant"),
        }
    }

    #[test]
    fn test_file_too_large_fields() {
        let err = Error::FileTooLarge {
            path: PathBuf::from("/tmp/large.bin"),
            size: 200 * 1024 * 1024,
            max_size: 100 * 1024 * 1024,
        };

        match err {
            Error::FileTooLarge {
                path,
                size,
                max_size,
            } => {
                assert_eq!(path.to_string_lossy(), "/tmp/large.bin");
                assert_eq!(size, 200 * 1024 * 1024);
                assert_eq!(max_size, 100 * 1024 * 1024);
            }
            _ => panic!("Unexpected error variant"),
        }
    }
}