luff 0.2.1

Print files with formatting
Documentation
//! Configuration-specific error types with rich diagnostics

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

/// Configuration-specific error types with rich diagnostics
// Suppress unused warnings: fields are used via #[error]/#[diagnostic] derives and tests
#[derive(Error, Debug)]
#[cfg_attr(feature = "cli", derive(Diagnostic))]
pub enum ConfigError {
    /// Configuration file not found
    #[error("Config file not found: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::file_not_found)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Check that the config file path is correct and the file exists"))
    )]
    FileNotFound {
        /// Path to the missing config file
        path: PathBuf,
        /// Underlying I/O error
        source: std::io::Error,
    },

    /// Configuration file is not a regular file
    #[error("Config file is not a regular file: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::not_a_file)))]
    NotAFile {
        /// Path that is not a regular file
        path: PathBuf,
    },

    /// Configuration file too large
    #[error("Config file too large: {path} ({size} bytes exceeds {max_size} bytes)")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::file_too_large)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Config files should be under 1MB. Check for accidental binary data."))
    )]
    FileTooLarge {
        /// Path to the oversized config file
        path: PathBuf,
        /// Actual size of the file
        size: u64,
        /// Maximum allowed size
        max_size: u64,
    },

    /// Configuration file path is invalid
    #[error("Invalid config file path: {path}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid_path)))]
    InvalidPath {
        /// The invalid path
        path: PathBuf,
        /// Underlying I/O error
        source: std::io::Error,
    },

    /// Configuration file has invalid file type
    #[error("Invalid config file type: {path}")]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Config file must be a regular file, not a symlink, FIFO, or socket"))
    )]
    InvalidFileType {
        /// Path with invalid file type
        path: PathBuf,
        /// Description of the invalid type
        message: String,
    },

    /// Configuration parse error
    #[error("Failed to parse config file {path}: {source}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::parse_error)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Check that the YAML syntax is valid and all values are correct"))
    )]
    ParseError {
        /// Path to the malformed config file
        path: PathBuf,
        /// Underlying parse error (boxed to reduce enum size)
        source: Box<figment::Error>,
    },

    /// Invalid `max_depth` value
    #[error("Invalid max_depth: {value} (must be between 0 and {max})")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid_max_depth)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Directory depth must be between 0 (unlimited) and 100"))
    )]
    InvalidMaxDepth {
        /// Invalid value provided
        value: usize,
        /// Maximum allowed value
        max: usize,
    },

    /// Invalid `max_files` value
    #[error("Invalid max_files: {value} (must be between 1 and {max})")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid_max_files)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("File limit prevents memory exhaustion. Maximum allowed is 10,000,000"))
    )]
    InvalidMaxFiles {
        /// Invalid value provided
        value: usize,
        /// Maximum allowed value
        max: usize,
    },

    /// Invalid `max_clipboard_mb` value
    #[error("Invalid max_clipboard_mb: {value} (must be between 0 and {max})")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid_max_clipboard)))]
    #[cfg_attr(
        feature = "cli",
        diagnostic(help("Clipboard limit prevents OOM. Maximum allowed is 1000MB"))
    )]
    InvalidMaxClipboardMb {
        /// Invalid value provided
        value: usize,
        /// Maximum allowed value
        max: usize,
    },

    /// Invalid pattern in configuration
    #[error("Invalid {pattern_type} pattern: {pattern}")]
    #[cfg_attr(feature = "cli", diagnostic(code(luff::config::invalid_pattern)))]
    InvalidPattern {
        /// The invalid pattern string
        pattern: String,
        /// Type of pattern (extension, directory, file)
        pattern_type: String,
        /// Help message for the user
        help: String,
    },
}

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

    #[test]
    fn test_config_error_display() {
        let err = ConfigError::InvalidMaxFiles {
            value: 20_000_000,
            max: 10_000_000,
        };
        let error_string = err.to_string();
        assert!(error_string.contains("20000000"));
        assert!(error_string.contains("10000000"));
        assert!(error_string.contains("max_files"));
    }

    #[test]
    fn test_config_file_error() {
        let path = PathBuf::from("/nonexistent/config.yaml");
        let err = ConfigError::FileNotFound {
            path,
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
        };
        assert!(err.to_string().contains("Config file not found"));
        assert!(err.to_string().contains("/nonexistent/config.yaml"));
    }

    #[test]
    fn test_invalid_pattern_fields() {
        let err = ConfigError::InvalidPattern {
            pattern: "*.png".to_string(),
            pattern_type: "extension".to_string(),
            help: "no globs allowed".to_string(),
        };

        match err {
            ConfigError::InvalidPattern {
                pattern,
                pattern_type,
                help,
            } => {
                assert_eq!(pattern, "*.png");
                assert_eq!(pattern_type, "extension");
                assert_eq!(help, "no globs allowed");
            }
            _ => panic!("Unexpected error variant"),
        }
    }

    #[test]
    fn test_invalid_max_depth() {
        let err = ConfigError::InvalidMaxDepth {
            value: 200,
            max: 100,
        };
        let msg = err.to_string();
        assert!(msg.contains("200"));
        assert!(msg.contains("100"));
    }

    #[test]
    fn test_invalid_max_clipboard_mb() {
        let err = ConfigError::InvalidMaxClipboardMb {
            value: 2000,
            max: 1000,
        };
        let msg = err.to_string();
        assert!(msg.contains("2000"));
        assert!(msg.contains("1000"));
    }

    #[test]
    fn test_file_too_large() {
        let err = ConfigError::FileTooLarge {
            path: PathBuf::from("/tmp/huge.yaml"),
            size: 2_000_000,
            max_size: 1_000_000,
        };
        let msg = err.to_string();
        assert!(msg.contains("/tmp/huge.yaml"));
        assert!(msg.contains("2000000"));
    }

    #[test]
    fn test_invalid_file_type() {
        let err = ConfigError::InvalidFileType {
            path: PathBuf::from("/tmp/fifo"),
            message: "is a FIFO".to_string(),
        };
        assert!(err.to_string().contains("/tmp/fifo"));
    }

    #[test]
    fn test_not_a_file() {
        let err = ConfigError::NotAFile {
            path: PathBuf::from("/tmp/dir"),
        };
        assert!(err.to_string().contains("not a regular file"));
    }
}