diff --git a/PROMPT.md b/PROMPT.md index 56ec076..610044a 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -333,7 +333,6 @@ Closes: 5. **Descriptive close reasons** - Document what was accomplished 6. **Sync bd frequently** - After every subtask completion 7. **Read the PLAN** - Always load context from PLAN.md before implementation -8. **Create changeset early** - Run `workspace changeset create` right after creating the feature branch --- diff --git a/crates/filesystem/src/lib.rs b/crates/filesystem/src/lib.rs index d1d4ea2..bcd7777 100644 --- a/crates/filesystem/src/lib.rs +++ b/crates/filesystem/src/lib.rs @@ -119,10 +119,12 @@ mod tests; // Error types pub use error::{Error, Result}; +// Types +pub use types::{DirEntry, FileType, Metadata}; + // TODO: Re-exports will be added as modules are implemented // pub use config::{FileSystemConfig, FileSystemConfigBuilder}; // pub use mock::MockFileSystem; // pub use path_ext::PathExt; // pub use real::RealFileSystem; // pub use traits::FileSystem; -// pub use types::{DirEntry, FileType, Metadata}; diff --git a/crates/filesystem/src/tests.rs b/crates/filesystem/src/tests.rs index 2bb8b1f..dfd5c23 100644 --- a/crates/filesystem/src/tests.rs +++ b/crates/filesystem/src/tests.rs @@ -402,7 +402,607 @@ mod config { #[cfg(test)] mod types { //! Tests for the types module. - // TODO: will be implemented on epic workspace-node-tools-3q8 (Types Module) + //! + //! This module contains unit tests for: + //! - `FileType` enum and its methods + //! - `Metadata` struct and its methods + //! - `DirEntry` struct and its methods + + use crate::types::FileType; + + // ========================================================================= + // FileType Enum Tests (FR-5.2.1 - FR-5.2.5) + // ========================================================================= + + // ------------------------------------------------------------------------- + // Variant Creation Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_file_type_file_variant() { + let ft = FileType::File; + assert!(ft.is_file()); + assert!(!ft.is_dir()); + assert!(!ft.is_symlink()); + } + + #[test] + fn test_file_type_dir_variant() { + let ft = FileType::Dir; + assert!(!ft.is_file()); + assert!(ft.is_dir()); + assert!(!ft.is_symlink()); + } + + #[test] + fn test_file_type_symlink_variant() { + let ft = FileType::Symlink; + assert!(!ft.is_file()); + assert!(!ft.is_dir()); + assert!(ft.is_symlink()); + } + + // ------------------------------------------------------------------------- + // Method Tests (FR-5.2.2 - FR-5.2.4) + // ------------------------------------------------------------------------- + + #[test] + fn test_is_file_returns_true_only_for_file() { + assert!(FileType::File.is_file()); + assert!(!FileType::Dir.is_file()); + assert!(!FileType::Symlink.is_file()); + } + + #[test] + fn test_is_dir_returns_true_only_for_dir() { + assert!(!FileType::File.is_dir()); + assert!(FileType::Dir.is_dir()); + assert!(!FileType::Symlink.is_dir()); + } + + #[test] + fn test_is_symlink_returns_true_only_for_symlink() { + assert!(!FileType::File.is_symlink()); + assert!(!FileType::Dir.is_symlink()); + assert!(FileType::Symlink.is_symlink()); + } + + // ------------------------------------------------------------------------- + // Trait Implementation Tests (FR-5.2.5) + // ------------------------------------------------------------------------- + + #[test] + fn test_file_type_debug() { + let file = FileType::File; + let debug_str = format!("{file:?}"); + assert_eq!(debug_str, "File"); + + let dir = FileType::Dir; + let debug_str = format!("{dir:?}"); + assert_eq!(debug_str, "Dir"); + + let symlink = FileType::Symlink; + let debug_str = format!("{symlink:?}"); + assert_eq!(debug_str, "Symlink"); + } + + #[test] + fn test_file_type_clone() { + fn assert_clone() {} + assert_clone::(); + + // For Copy types, clone is equivalent to copy + let original = FileType::File; + let cloned: FileType = Clone::clone(&original); + assert_eq!(original, cloned); + } + + #[test] + fn test_file_type_copy() { + let original = FileType::Dir; + let copied = original; // Copy, not move + assert_eq!(original, copied); + // Both can still be used (proving Copy works) + assert!(original.is_dir()); + assert!(copied.is_dir()); + } + + #[test] + fn test_file_type_partial_eq() { + assert_eq!(FileType::File, FileType::File); + assert_eq!(FileType::Dir, FileType::Dir); + assert_eq!(FileType::Symlink, FileType::Symlink); + + assert_ne!(FileType::File, FileType::Dir); + assert_ne!(FileType::File, FileType::Symlink); + assert_ne!(FileType::Dir, FileType::Symlink); + } + + #[test] + fn test_file_type_eq() { + // Eq is a marker trait, we just verify it's implemented + fn assert_eq_impl() {} + assert_eq_impl::(); + } + + // ------------------------------------------------------------------------- + // Trait Bound Verification Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_file_type_is_send() { + fn assert_send() {} + assert_send::(); + } + + #[test] + fn test_file_type_is_sync() { + fn assert_sync() {} + assert_sync::(); + } + + // ------------------------------------------------------------------------- + // From Tests + // ------------------------------------------------------------------------- + + // Note: Testing From directly requires actual filesystem + // access to obtain a std::fs::FileType instance. These tests are covered + // in integration tests. Here we verify the trait is implemented. + + #[test] + fn test_file_type_from_trait_is_implemented() { + fn assert_from>() {} + assert_from::(); + } + + // ========================================================================= + // Metadata Struct Tests (FR-5.3.1 - FR-5.3.6) + // ========================================================================= + + use crate::types::Metadata; + + // ------------------------------------------------------------------------- + // Constructor Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_new_file() { + let metadata = Metadata::new(FileType::File, 1024); + assert!(metadata.is_file()); + assert_eq!(metadata.len(), 1024); + } + + #[test] + fn test_metadata_new_dir() { + let metadata = Metadata::new(FileType::Dir, 0); + assert!(metadata.is_dir()); + assert_eq!(metadata.len(), 0); + } + + #[test] + fn test_metadata_new_symlink() { + let metadata = Metadata::new(FileType::Symlink, 42); + assert!(metadata.is_symlink()); + assert_eq!(metadata.len(), 42); + } + + // ------------------------------------------------------------------------- + // len() Method Tests (FR-5.3.1) + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_len_zero() { + let metadata = Metadata::new(FileType::File, 0); + assert_eq!(metadata.len(), 0); + } + + #[test] + fn test_metadata_len_small_file() { + let metadata = Metadata::new(FileType::File, 100); + assert_eq!(metadata.len(), 100); + } + + #[test] + fn test_metadata_len_large_file() { + let large_size: u64 = 10 * 1024 * 1024 * 1024; // 10 GB + let metadata = Metadata::new(FileType::File, large_size); + assert_eq!(metadata.len(), large_size); + } + + #[test] + fn test_metadata_len_max_u64() { + let metadata = Metadata::new(FileType::File, u64::MAX); + assert_eq!(metadata.len(), u64::MAX); + } + + // ------------------------------------------------------------------------- + // is_empty() Method Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_is_empty_true() { + let metadata = Metadata::new(FileType::File, 0); + assert!(metadata.is_empty()); + } + + #[test] + fn test_metadata_is_empty_false() { + let metadata = Metadata::new(FileType::File, 1); + assert!(!metadata.is_empty()); + } + + #[test] + fn test_metadata_is_empty_large_file() { + let metadata = Metadata::new(FileType::File, 1_000_000); + assert!(!metadata.is_empty()); + } + + // ------------------------------------------------------------------------- + // file_type() Method Tests (FR-5.3.5) + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_file_type_file() { + let metadata = Metadata::new(FileType::File, 100); + assert_eq!(metadata.file_type(), FileType::File); + } + + #[test] + fn test_metadata_file_type_dir() { + let metadata = Metadata::new(FileType::Dir, 0); + assert_eq!(metadata.file_type(), FileType::Dir); + } + + #[test] + fn test_metadata_file_type_symlink() { + let metadata = Metadata::new(FileType::Symlink, 0); + assert_eq!(metadata.file_type(), FileType::Symlink); + } + + // ------------------------------------------------------------------------- + // is_file() Method Tests (FR-5.3.2) + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_is_file_true() { + let metadata = Metadata::new(FileType::File, 100); + assert!(metadata.is_file()); + } + + #[test] + fn test_metadata_is_file_false_for_dir() { + let metadata = Metadata::new(FileType::Dir, 0); + assert!(!metadata.is_file()); + } + + #[test] + fn test_metadata_is_file_false_for_symlink() { + let metadata = Metadata::new(FileType::Symlink, 0); + assert!(!metadata.is_file()); + } + + // ------------------------------------------------------------------------- + // is_dir() Method Tests (FR-5.3.3) + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_is_dir_true() { + let metadata = Metadata::new(FileType::Dir, 0); + assert!(metadata.is_dir()); + } + + #[test] + fn test_metadata_is_dir_false_for_file() { + let metadata = Metadata::new(FileType::File, 100); + assert!(!metadata.is_dir()); + } + + #[test] + fn test_metadata_is_dir_false_for_symlink() { + let metadata = Metadata::new(FileType::Symlink, 0); + assert!(!metadata.is_dir()); + } + + // ------------------------------------------------------------------------- + // is_symlink() Method Tests (FR-5.3.4) + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_is_symlink_true() { + let metadata = Metadata::new(FileType::Symlink, 0); + assert!(metadata.is_symlink()); + } + + #[test] + fn test_metadata_is_symlink_false_for_file() { + let metadata = Metadata::new(FileType::File, 100); + assert!(!metadata.is_symlink()); + } + + #[test] + fn test_metadata_is_symlink_false_for_dir() { + let metadata = Metadata::new(FileType::Dir, 0); + assert!(!metadata.is_symlink()); + } + + // ------------------------------------------------------------------------- + // Trait Implementation Tests (FR-5.3.6) + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_debug() { + let metadata = Metadata::new(FileType::File, 1024); + let debug_str = format!("{metadata:?}"); + assert!(debug_str.contains("Metadata")); + assert!(debug_str.contains("File")); + assert!(debug_str.contains("1024")); + } + + #[test] + fn test_metadata_clone() { + let original = Metadata::new(FileType::File, 2048); + let cloned = original.clone(); + assert_eq!(original.file_type(), cloned.file_type()); + assert_eq!(original.len(), cloned.len()); + } + + #[test] + fn test_metadata_clone_independence() { + let original = Metadata::new(FileType::Dir, 0); + let cloned = original.clone(); + // Both can be used independently + assert!(original.is_dir()); + assert!(cloned.is_dir()); + } + + // ------------------------------------------------------------------------- + // Trait Bound Verification Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_metadata_is_send() { + fn assert_send() {} + assert_send::(); + } + + #[test] + fn test_metadata_is_sync() { + fn assert_sync() {} + assert_sync::(); + } + + // ------------------------------------------------------------------------- + // From Tests + // ------------------------------------------------------------------------- + + // Note: Testing From directly requires actual filesystem + // access to obtain a std::fs::Metadata instance. These tests are covered + // in integration tests. Here we verify the trait is implemented. + + #[test] + fn test_metadata_from_trait_is_implemented() { + fn assert_from>() {} + assert_from::(); + } + + // ========================================================================= + // DirEntry Struct Tests (FR-5.1.1 - FR-5.1.4) + // ========================================================================= + + use crate::types::DirEntry; + use std::ffi::OsStr; + use std::path::PathBuf; + + // ------------------------------------------------------------------------- + // Constructor Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_new_file() { + let path = PathBuf::from("/home/user/file.txt"); + let entry = DirEntry::new(path.clone(), FileType::File); + assert_eq!(entry.path(), path.as_path()); + assert!(entry.file_type().is_file()); + } + + #[test] + fn test_dir_entry_new_dir() { + let path = PathBuf::from("/home/user/documents"); + let entry = DirEntry::new(path.clone(), FileType::Dir); + assert_eq!(entry.path(), path.as_path()); + assert!(entry.file_type().is_dir()); + } + + #[test] + fn test_dir_entry_new_symlink() { + let path = PathBuf::from("/home/user/link"); + let entry = DirEntry::new(path.clone(), FileType::Symlink); + assert_eq!(entry.path(), path.as_path()); + assert!(entry.file_type().is_symlink()); + } + + // ------------------------------------------------------------------------- + // path() Method Tests (FR-5.1.1) + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_path_absolute() { + let path = PathBuf::from("/absolute/path/to/file.txt"); + let entry = DirEntry::new(path.clone(), FileType::File); + assert_eq!(entry.path(), path.as_path()); + } + + #[test] + fn test_dir_entry_path_relative() { + let path = PathBuf::from("relative/path/file.txt"); + let entry = DirEntry::new(path.clone(), FileType::File); + assert_eq!(entry.path(), path.as_path()); + } + + #[test] + fn test_dir_entry_path_with_special_chars() { + let path = PathBuf::from("/path/with spaces/and-dashes/file_name.txt"); + let entry = DirEntry::new(path.clone(), FileType::File); + assert_eq!(entry.path(), path.as_path()); + } + + #[test] + fn test_dir_entry_path_unicode() { + let path = PathBuf::from("/путь/文件/αρχείο.txt"); + let entry = DirEntry::new(path.clone(), FileType::File); + assert_eq!(entry.path(), path.as_path()); + } + + // ------------------------------------------------------------------------- + // file_name() Method Tests (FR-5.1.2) + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_file_name_simple() { + let entry = DirEntry::new(PathBuf::from("/home/user/document.pdf"), FileType::File); + assert_eq!(entry.file_name(), OsStr::new("document.pdf")); + } + + #[test] + fn test_dir_entry_file_name_directory() { + let entry = DirEntry::new(PathBuf::from("/var/log"), FileType::Dir); + assert_eq!(entry.file_name(), OsStr::new("log")); + } + + #[test] + fn test_dir_entry_file_name_with_extension() { + let entry = DirEntry::new(PathBuf::from("/path/to/archive.tar.gz"), FileType::File); + assert_eq!(entry.file_name(), OsStr::new("archive.tar.gz")); + } + + #[test] + fn test_dir_entry_file_name_hidden_file() { + let entry = DirEntry::new(PathBuf::from("/home/user/.bashrc"), FileType::File); + assert_eq!(entry.file_name(), OsStr::new(".bashrc")); + } + + #[test] + fn test_dir_entry_file_name_root_path() { + // Root path has no file name, should return empty OsStr + let entry = DirEntry::new(PathBuf::from("/"), FileType::Dir); + assert_eq!(entry.file_name(), OsStr::new("")); + } + + #[test] + fn test_dir_entry_file_name_dot_dot() { + // Parent directory reference has no file name + let entry = DirEntry::new(PathBuf::from(".."), FileType::Dir); + assert_eq!(entry.file_name(), OsStr::new("")); + } + + #[test] + fn test_dir_entry_file_name_relative() { + let entry = DirEntry::new(PathBuf::from("relative/path/file.txt"), FileType::File); + assert_eq!(entry.file_name(), OsStr::new("file.txt")); + } + + // ------------------------------------------------------------------------- + // file_type() Method Tests (FR-5.1.3) + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_file_type_file() { + let entry = DirEntry::new(PathBuf::from("/tmp/file.txt"), FileType::File); + assert_eq!(entry.file_type(), FileType::File); + assert!(entry.file_type().is_file()); + assert!(!entry.file_type().is_dir()); + assert!(!entry.file_type().is_symlink()); + } + + #[test] + fn test_dir_entry_file_type_dir() { + let entry = DirEntry::new(PathBuf::from("/tmp/subdir"), FileType::Dir); + assert_eq!(entry.file_type(), FileType::Dir); + assert!(!entry.file_type().is_file()); + assert!(entry.file_type().is_dir()); + assert!(!entry.file_type().is_symlink()); + } + + #[test] + fn test_dir_entry_file_type_symlink() { + let entry = DirEntry::new(PathBuf::from("/tmp/link"), FileType::Symlink); + assert_eq!(entry.file_type(), FileType::Symlink); + assert!(!entry.file_type().is_file()); + assert!(!entry.file_type().is_dir()); + assert!(entry.file_type().is_symlink()); + } + + // ------------------------------------------------------------------------- + // Trait Implementation Tests (FR-5.1.4) + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_debug() { + let entry = DirEntry::new(PathBuf::from("/test/path.txt"), FileType::File); + let debug_str = format!("{entry:?}"); + assert!(debug_str.contains("DirEntry")); + assert!(debug_str.contains("path")); + assert!(debug_str.contains("file_type")); + } + + #[test] + fn test_dir_entry_clone() { + let original = DirEntry::new(PathBuf::from("/original/path.txt"), FileType::File); + let cloned = original.clone(); + assert_eq!(original.path(), cloned.path()); + assert_eq!(original.file_type(), cloned.file_type()); + } + + #[test] + fn test_dir_entry_clone_independence() { + let original = DirEntry::new(PathBuf::from("/some/path"), FileType::Dir); + let cloned = original.clone(); + // Both can be used independently + assert!(original.file_type().is_dir()); + assert!(cloned.file_type().is_dir()); + assert_eq!(original.file_name(), cloned.file_name()); + } + + // ------------------------------------------------------------------------- + // Trait Bound Verification Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_is_send() { + fn assert_send() {} + assert_send::(); + } + + #[test] + fn test_dir_entry_is_sync() { + fn assert_sync() {} + assert_sync::(); + } + + // ------------------------------------------------------------------------- + // Edge Case Tests + // ------------------------------------------------------------------------- + + #[test] + fn test_dir_entry_empty_path() { + let entry = DirEntry::new(PathBuf::new(), FileType::File); + assert_eq!(entry.path(), PathBuf::new().as_path()); + assert_eq!(entry.file_name(), OsStr::new("")); + } + + #[test] + fn test_dir_entry_single_component() { + let entry = DirEntry::new(PathBuf::from("filename.txt"), FileType::File); + assert_eq!(entry.file_name(), OsStr::new("filename.txt")); + } + + #[test] + fn test_dir_entry_trailing_slash() { + // PathBuf normalizes trailing slashes + let entry = DirEntry::new(PathBuf::from("/path/to/dir/"), FileType::Dir); + // Note: PathBuf may preserve or remove trailing slash depending on platform + assert!(entry.file_type().is_dir()); + } } #[cfg(test)] diff --git a/crates/filesystem/src/types.rs b/crates/filesystem/src/types.rs index 9aa91c9..c977c97 100644 --- a/crates/filesystem/src/types.rs +++ b/crates/filesystem/src/types.rs @@ -7,7 +7,7 @@ //! This module provides three fundamental types used throughout the crate: //! - [`FileType`]: Discriminates between files, directories, and symlinks //! - [`DirEntry`]: Represents a single entry when listing directory contents -//! - [`Metadata`]: Provides file/directory metadata (size, timestamps, permissions) +//! - [`Metadata`]: Provides file/directory metadata (size, type) //! //! ## How //! @@ -30,15 +30,600 @@ //! ## Example //! //! ```rust,ignore -//! use workspace_fs::{DirEntry, FileType, Metadata}; -//! -//! async fn list_files(fs: &impl FileSystem, dir: &Path) -> Result> { -//! let entries = fs.read_dir(dir).await?; -//! Ok(entries.into_iter() -//! .filter(|e| e.file_type() == FileType::File) -//! .collect()) -//! } +//! use workspace_fs::{FileType, DirEntry, Metadata}; +//! +//! let file_type = FileType::File; +//! assert!(file_type.is_file()); +//! assert!(!file_type.is_dir()); +//! assert!(!file_type.is_symlink()); +//! +//! let dir_type = FileType::Dir; +//! assert!(dir_type.is_dir()); //! ``` -// TODO: will be implemented on epic workspace-node-tools-3q8 (Types Module) -#![allow(clippy::todo)] +// ============================================================================= +// FileType Enum +// ============================================================================= + +/// Represents the type of a filesystem entry. +/// +/// This enum provides a simple, unified representation of filesystem entry types +/// across all platforms. It normalizes the differences between how various +/// operating systems report file types. +/// +/// # Variants +/// +/// | Variant | Description | +/// |---------|-------------| +/// | [`File`][Self::File] | A regular file containing data | +/// | [`Dir`][Self::Dir] | A directory that can contain other entries | +/// | [`Symlink`][Self::Symlink] | A symbolic link pointing to another path | +/// +/// # Trait Implementations +/// +/// - [`Debug`]: Formats the variant name for debugging +/// - [`Clone`] and [`Copy`]: Allows copying by value (zero-cost) +/// - [`PartialEq`] and [`Eq`]: Enables equality comparisons +/// - [`From`]: Converts from the standard library type +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::FileType; +/// +/// // Create and check file types +/// let file = FileType::File; +/// assert!(file.is_file()); +/// +/// let dir = FileType::Dir; +/// assert!(dir.is_dir()); +/// +/// let symlink = FileType::Symlink; +/// assert!(symlink.is_symlink()); +/// +/// // Types are comparable +/// assert_eq!(FileType::File, FileType::File); +/// assert_ne!(FileType::File, FileType::Dir); +/// +/// // Types are copyable +/// let copy = file; +/// assert_eq!(file, copy); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileType { + /// A regular file containing data. + /// + /// This variant represents any file that is not a directory or symbolic link. + /// It includes text files, binary files, executables, and any other regular + /// file type. + File, + + /// A directory that can contain other filesystem entries. + /// + /// Directories are containers for files, other directories, and symbolic links. + /// They form the hierarchical structure of the filesystem. + Dir, + + /// A symbolic link pointing to another path. + /// + /// Symbolic links are special filesystem entries that reference another path. + /// The target path may or may not exist, and may be a file, directory, or + /// another symbolic link. + Symlink, +} + +impl FileType { + /// Returns `true` if this is a regular file. + /// + /// This method checks whether the entry is a regular file (not a directory + /// or symbolic link). + /// + /// # Returns + /// + /// - `true` if the type is [`FileType::File`] + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::FileType; + /// + /// assert!(FileType::File.is_file()); + /// assert!(!FileType::Dir.is_file()); + /// assert!(!FileType::Symlink.is_file()); + /// ``` + #[must_use] + pub fn is_file(self) -> bool { + matches!(self, Self::File) + } + + /// Returns `true` if this is a directory. + /// + /// This method checks whether the entry is a directory. + /// + /// # Returns + /// + /// - `true` if the type is [`FileType::Dir`] + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::FileType; + /// + /// assert!(FileType::Dir.is_dir()); + /// assert!(!FileType::File.is_dir()); + /// assert!(!FileType::Symlink.is_dir()); + /// ``` + #[must_use] + pub fn is_dir(self) -> bool { + matches!(self, Self::Dir) + } + + /// Returns `true` if this is a symbolic link. + /// + /// This method checks whether the entry is a symbolic link. + /// + /// # Returns + /// + /// - `true` if the type is [`FileType::Symlink`] + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::FileType; + /// + /// assert!(FileType::Symlink.is_symlink()); + /// assert!(!FileType::File.is_symlink()); + /// assert!(!FileType::Dir.is_symlink()); + /// ``` + #[must_use] + pub fn is_symlink(self) -> bool { + matches!(self, Self::Symlink) + } +} + +// ============================================================================= +// Conversions +// ============================================================================= + +/// Converts from [`std::fs::FileType`] to [`FileType`]. +/// +/// This implementation allows seamless conversion from the standard library's +/// file type representation to this crate's abstraction. +/// +/// # Conversion Logic +/// +/// The conversion follows this priority order: +/// 1. If [`std::fs::FileType::is_symlink()`] returns `true` → [`FileType::Symlink`] +/// 2. If [`std::fs::FileType::is_dir()`] returns `true` → [`FileType::Dir`] +/// 3. Otherwise → [`FileType::File`] +/// +/// The symlink check comes first because symbolic links can sometimes appear +/// as files or directories depending on how metadata is retrieved (with or +/// without following links). +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::FileType; +/// use std::fs; +/// +/// let metadata = fs::metadata("some_file.txt")?; +/// let file_type: FileType = metadata.file_type().into(); +/// ``` +impl From for FileType { + fn from(ft: std::fs::FileType) -> Self { + if ft.is_symlink() { + Self::Symlink + } else if ft.is_dir() { + Self::Dir + } else { + Self::File + } + } +} + +// ============================================================================= +// Metadata Struct +// ============================================================================= + +/// Metadata information about a filesystem entry. +/// +/// This struct provides a platform-agnostic representation of file metadata, +/// including the file type and size. It abstracts over the differences between +/// how various operating systems report file metadata. +/// +/// # Fields +/// +/// The struct contains the following private fields, accessible through getter methods: +/// - `file_type`: The type of the filesystem entry ([`FileType`]) +/// - `len`: The size of the file in bytes (always 0 for directories) +/// +/// # Trait Implementations +/// +/// - [`Debug`]: Formats the metadata for debugging +/// - [`Clone`]: Allows cloning the metadata +/// - [`From`]: Converts from the standard library type +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::{Metadata, FileType}; +/// +/// // Create metadata for a 1024-byte file +/// let metadata = Metadata::new(FileType::File, 1024); +/// assert!(metadata.is_file()); +/// assert_eq!(metadata.len(), 1024); +/// assert!(!metadata.is_empty()); +/// +/// // Create metadata for an empty directory +/// let dir_metadata = Metadata::new(FileType::Dir, 0); +/// assert!(dir_metadata.is_dir()); +/// assert!(dir_metadata.is_empty()); +/// ``` +#[derive(Debug, Clone)] +pub struct Metadata { + /// The type of the filesystem entry. + file_type: FileType, + /// The size of the file in bytes. + /// + /// For directories, this value is typically 0 or represents the + /// directory's metadata size (platform-dependent). + len: u64, +} + +impl Metadata { + /// Creates a new `Metadata` instance. + /// + /// # Arguments + /// + /// * `file_type` - The type of the filesystem entry + /// * `len` - The size of the file in bytes + /// + /// # Returns + /// + /// A new `Metadata` instance with the specified file type and length. + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let metadata = Metadata::new(FileType::File, 2048); + /// assert!(metadata.is_file()); + /// assert_eq!(metadata.len(), 2048); + /// ``` + #[must_use] + pub fn new(file_type: FileType, len: u64) -> Self { + Self { file_type, len } + } + + /// Returns the size of the file in bytes. + /// + /// For regular files, this returns the actual file size. For directories, + /// the value is platform-dependent and typically represents the directory + /// metadata size (often 0). + /// + /// # Returns + /// + /// The size of the file in bytes as a `u64`. + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let metadata = Metadata::new(FileType::File, 1024); + /// assert_eq!(metadata.len(), 1024); + /// + /// let empty_file = Metadata::new(FileType::File, 0); + /// assert_eq!(empty_file.len(), 0); + /// ``` + #[must_use] + pub fn len(&self) -> u64 { + self.len + } + + /// Returns `true` if the file size is zero. + /// + /// This is useful for quickly checking if a file is empty without + /// reading its contents. + /// + /// # Returns + /// + /// - `true` if the file size is 0 + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let empty = Metadata::new(FileType::File, 0); + /// assert!(empty.is_empty()); + /// + /// let not_empty = Metadata::new(FileType::File, 100); + /// assert!(!not_empty.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Returns the file type. + /// + /// # Returns + /// + /// The [`FileType`] of this filesystem entry. + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let metadata = Metadata::new(FileType::Dir, 0); + /// assert_eq!(metadata.file_type(), FileType::Dir); + /// ``` + #[must_use] + pub fn file_type(&self) -> FileType { + self.file_type + } + + /// Returns `true` if this is a regular file. + /// + /// This is a convenience method equivalent to calling + /// `self.file_type().is_file()`. + /// + /// # Returns + /// + /// - `true` if the entry is a regular file + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let file_meta = Metadata::new(FileType::File, 1024); + /// assert!(file_meta.is_file()); + /// + /// let dir_meta = Metadata::new(FileType::Dir, 0); + /// assert!(!dir_meta.is_file()); + /// ``` + #[must_use] + pub fn is_file(&self) -> bool { + self.file_type.is_file() + } + + /// Returns `true` if this is a directory. + /// + /// This is a convenience method equivalent to calling + /// `self.file_type().is_dir()`. + /// + /// # Returns + /// + /// - `true` if the entry is a directory + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let dir_meta = Metadata::new(FileType::Dir, 0); + /// assert!(dir_meta.is_dir()); + /// + /// let file_meta = Metadata::new(FileType::File, 1024); + /// assert!(!file_meta.is_dir()); + /// ``` + #[must_use] + pub fn is_dir(&self) -> bool { + self.file_type.is_dir() + } + + /// Returns `true` if this is a symbolic link. + /// + /// This is a convenience method equivalent to calling + /// `self.file_type().is_symlink()`. + /// + /// # Returns + /// + /// - `true` if the entry is a symbolic link + /// - `false` otherwise + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{Metadata, FileType}; + /// + /// let symlink_meta = Metadata::new(FileType::Symlink, 0); + /// assert!(symlink_meta.is_symlink()); + /// + /// let file_meta = Metadata::new(FileType::File, 1024); + /// assert!(!file_meta.is_symlink()); + /// ``` + #[must_use] + pub fn is_symlink(&self) -> bool { + self.file_type.is_symlink() + } +} + +/// Converts from [`std::fs::Metadata`] to [`Metadata`]. +/// +/// This implementation allows seamless conversion from the standard library's +/// metadata type to this crate's abstraction. +/// +/// # Conversion Details +/// +/// - `file_type`: Converted using the `From` implementation +/// - `len`: Obtained from [`std::fs::Metadata::len()`] +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::Metadata; +/// use std::fs; +/// +/// let std_metadata = fs::metadata("some_file.txt")?; +/// let metadata: Metadata = std_metadata.into(); +/// println!("File size: {} bytes", metadata.len()); +/// ``` +impl From for Metadata { + fn from(meta: std::fs::Metadata) -> Self { + Self { file_type: meta.file_type().into(), len: meta.len() } + } +} + +// ============================================================================= +// DirEntry Struct +// ============================================================================= + +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +/// Represents an entry in a directory. +/// +/// This struct provides a unified representation of directory entries across +/// all platforms. It contains the full path to the entry and its file type, +/// which are the essential pieces of information needed when listing directory +/// contents. +/// +/// # Fields +/// +/// The struct contains the following private fields, accessible through getter methods: +/// - `path`: The full path to the filesystem entry ([`PathBuf`]) +/// - `file_type`: The type of the entry ([`FileType`]) +/// +/// # Trait Implementations +/// +/// - [`Debug`]: Formats the entry for debugging +/// - [`Clone`]: Allows cloning the entry +/// +/// # Example +/// +/// ```rust,ignore +/// use workspace_fs::{DirEntry, FileType}; +/// use std::path::PathBuf; +/// +/// // Create a directory entry for a file +/// let entry = DirEntry::new(PathBuf::from("/home/user/file.txt"), FileType::File); +/// assert_eq!(entry.path().to_str(), Some("/home/user/file.txt")); +/// assert_eq!(entry.file_name().to_str(), Some("file.txt")); +/// assert!(entry.file_type().is_file()); +/// +/// // Create a directory entry for a directory +/// let dir_entry = DirEntry::new(PathBuf::from("/home/user/docs"), FileType::Dir); +/// assert!(dir_entry.file_type().is_dir()); +/// ``` +#[derive(Debug, Clone)] +pub struct DirEntry { + /// The full path to this filesystem entry. + path: PathBuf, + /// The type of this filesystem entry. + file_type: FileType, +} + +impl DirEntry { + /// Creates a new `DirEntry` instance. + /// + /// # Arguments + /// + /// * `path` - The full path to the filesystem entry + /// * `file_type` - The type of the entry (file, directory, or symlink) + /// + /// # Returns + /// + /// A new `DirEntry` instance with the specified path and file type. + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{DirEntry, FileType}; + /// use std::path::PathBuf; + /// + /// let entry = DirEntry::new(PathBuf::from("/tmp/test.txt"), FileType::File); + /// assert!(entry.file_type().is_file()); + /// ``` + #[must_use] + pub fn new(path: PathBuf, file_type: FileType) -> Self { + Self { path, file_type } + } + + /// Returns the full path of this entry. + /// + /// This returns a reference to the complete path, including all parent + /// directories and the file name. + /// + /// # Returns + /// + /// A reference to the [`Path`] of this entry. + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{DirEntry, FileType}; + /// use std::path::PathBuf; + /// + /// let entry = DirEntry::new(PathBuf::from("/home/user/docs/file.txt"), FileType::File); + /// assert_eq!(entry.path().to_str(), Some("/home/user/docs/file.txt")); + /// ``` + #[must_use] + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the file name of this entry. + /// + /// This extracts just the final component of the path (the file or directory + /// name without the parent path). For paths that don't have a file name + /// component (like `/` or `..`), this returns an empty [`OsStr`]. + /// + /// # Returns + /// + /// A reference to the file name as an [`OsStr`]. Returns an empty `OsStr` + /// if the path has no file name component. + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{DirEntry, FileType}; + /// use std::path::PathBuf; + /// + /// let entry = DirEntry::new(PathBuf::from("/home/user/document.pdf"), FileType::File); + /// assert_eq!(entry.file_name().to_str(), Some("document.pdf")); + /// + /// let dir_entry = DirEntry::new(PathBuf::from("/var/log"), FileType::Dir); + /// assert_eq!(dir_entry.file_name().to_str(), Some("log")); + /// ``` + #[must_use] + pub fn file_name(&self) -> &OsStr { + self.path.file_name().unwrap_or_else(|| OsStr::new("")) + } + + /// Returns the file type of this entry. + /// + /// # Returns + /// + /// The [`FileType`] of this entry (file, directory, or symlink). + /// + /// # Example + /// + /// ```rust,ignore + /// use workspace_fs::{DirEntry, FileType}; + /// use std::path::PathBuf; + /// + /// let file_entry = DirEntry::new(PathBuf::from("/tmp/file.txt"), FileType::File); + /// assert_eq!(file_entry.file_type(), FileType::File); + /// assert!(file_entry.file_type().is_file()); + /// + /// let dir_entry = DirEntry::new(PathBuf::from("/tmp/subdir"), FileType::Dir); + /// assert_eq!(dir_entry.file_type(), FileType::Dir); + /// assert!(dir_entry.file_type().is_dir()); + /// ``` + #[must_use] + pub fn file_type(&self) -> FileType { + self.file_type + } +}