diff --git a/crates/filesystem/Cargo.toml b/crates/filesystem/Cargo.toml index 1be572e..7935f50 100644 --- a/crates/filesystem/Cargo.toml +++ b/crates/filesystem/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" async-trait.workspace = true log.workspace = true snafu.workspace = true -tokio = { workspace = true, features = ["fs", "sync", "io-util"] } +tokio = { workspace = true, features = ["fs", "sync", "io-util", "time"] } [dev-dependencies] tempfile.workspace = true diff --git a/crates/filesystem/src/config.rs b/crates/filesystem/src/config.rs new file mode 100644 index 0000000..fb3f845 --- /dev/null +++ b/crates/filesystem/src/config.rs @@ -0,0 +1,47 @@ +//! # Configuration Module +//! +//! Provides configuration types for filesystem operations, including timeout settings. +//! +//! ## What +//! +//! This module defines [`FileSystemConfig`] and its builder, which control the behavior +//! of filesystem operations. The primary configuration options are timeout values for +//! different operation categories. +//! +//! ## How +//! +//! Configuration is created using the builder pattern: +//! 1. Call `FileSystemConfig::builder()` to get a `FileSystemConfigBuilder` +//! 2. Chain configuration methods (e.g., `with_read_timeout()`) +//! 3. Call `build()` to obtain the final `FileSystemConfig` +//! +//! The configuration is immutable once built, ensuring thread-safety and preventing +//! accidental modification during filesystem operations. +//! +//! ## Why +//! +//! Configurable timeouts are essential for: +//! - **Reliability**: Prevent operations from hanging on unresponsive filesystems +//! - **Flexibility**: Different use cases require different timeout values +//! - **Testability**: Mock filesystems can use shorter timeouts for faster tests +//! - **Predictability**: Known timeout behavior helps with error handling +//! +//! ## Example +//! +//! ```rust,ignore +//! use workspace_fs::FileSystemConfig; +//! use std::time::Duration; +//! +//! // Use default configuration (30s read/write, 60s operation) +//! let config = FileSystemConfig::default(); +//! +//! // Custom configuration with builder +//! let config = FileSystemConfig::builder() +//! .with_read_timeout(Duration::from_secs(10)) +//! .with_write_timeout(Duration::from_secs(10)) +//! .with_operation_timeout(Duration::from_secs(30)) +//! .build(); +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-g2t (Configuration Module) +#![allow(clippy::todo)] diff --git a/crates/filesystem/src/error.rs b/crates/filesystem/src/error.rs new file mode 100644 index 0000000..63d92f7 --- /dev/null +++ b/crates/filesystem/src/error.rs @@ -0,0 +1,42 @@ +//! # Error Module +//! +//! Defines the unified error type for all filesystem operations in workspace-fs. +//! +//! ## What +//! +//! This module provides a single [`Error`] enum that captures all possible error +//! conditions that can occur during filesystem operations. It uses the `snafu` crate +//! for ergonomic error handling with context. +//! +//! ## How +//! +//! The error type is built using `snafu`'s derive macro, which automatically generates: +//! - `Error` and `Display` implementations +//! - Context selectors for each variant +//! - Backtrace capture (when enabled) +//! +//! Each variant includes the path that caused the error and, where applicable, the +//! underlying system error. This provides rich debugging information. +//! +//! ## Why +//! +//! A unified error type ensures: +//! - **Consistency**: All filesystem operations return the same error type +//! - **Context Preservation**: Every error includes the path and operation that failed +//! - **Type Safety**: Compile-time guarantees about error handling +//! - **Ergonomics**: Easy error conversion with the `?` operator +//! +//! ## Example +//! +//! ```rust,ignore +//! use workspace_fs::{Error, Result}; +//! use std::path::Path; +//! +//! async fn read_config(path: &Path) -> Result { +//! // Errors automatically include the path that failed +//! fs.read_to_string(path).await +//! } +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-906 (Error Module) +#![allow(clippy::todo)] diff --git a/crates/filesystem/src/lib.rs b/crates/filesystem/src/lib.rs index 1044caa..77df821 100644 --- a/crates/filesystem/src/lib.rs +++ b/crates/filesystem/src/lib.rs @@ -43,11 +43,84 @@ //! } //! ``` +// ============================================================================= +// Safety: This crate contains no unsafe code (NFR-4.1) +// ============================================================================= +#![forbid(unsafe_code)] +// ============================================================================= +// Documentation lints +// ============================================================================= #![warn(missing_docs)] #![warn(rustdoc::missing_crate_level_docs)] +// ============================================================================= +// Code quality lints (deny unwrap/expect/todo/unimplemented/panic in production) +// ============================================================================= #![deny(unused_must_use)] #![deny(clippy::unwrap_used)] #![deny(clippy::expect_used)] #![deny(clippy::todo)] #![deny(clippy::unimplemented)] #![deny(clippy::panic)] + +// ============================================================================= +// Module Declarations +// ============================================================================= + +/// Error types for filesystem operations. +/// +/// Provides a unified [`Error`](error) enum that captures all possible error +/// conditions with path context for debugging. +pub(crate) mod error; + +/// Configuration types for filesystem behavior. +/// +/// Provides [`FileSystemConfig`](config) with timeout settings and a builder +/// pattern for customization. +pub(crate) mod config; + +/// Core data types for filesystem entries. +/// +/// Provides [`FileType`](types), [`DirEntry`](types), and [`Metadata`](types) +/// types used throughout the crate. +pub(crate) mod types; + +/// The core filesystem trait definition. +/// +/// Defines the [`FileSystem`](traits) trait that abstracts all filesystem +/// operations for dependency injection and testing. +pub(crate) mod traits; + +/// Path utility extensions. +/// +/// Provides the [`PathExt`](path_ext) trait with synchronous path manipulation +/// utilities that don't perform I/O. +pub(crate) mod path_ext; + +/// Real filesystem implementation using tokio::fs. +/// +/// Provides [`RealFileSystem`](real) for production use with async I/O +/// and configurable timeout support. +pub(crate) mod real; + +/// Mock filesystem implementation for testing. +/// +/// Provides [`MockFileSystem`](mock) for fast, deterministic unit tests +/// without touching the disk. +pub(crate) mod mock; + +/// Unit tests organized by module. +#[cfg(test)] +mod tests; + +// ============================================================================= +// Public Re-exports +// ============================================================================= + +// TODO: Re-exports will be added as modules are implemented +// pub use config::{FileSystemConfig, FileSystemConfigBuilder}; +// pub use error::{Error, Result}; +// 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/mock.rs b/crates/filesystem/src/mock.rs new file mode 100644 index 0000000..d895fb7 --- /dev/null +++ b/crates/filesystem/src/mock.rs @@ -0,0 +1,67 @@ +//! # Mock FileSystem Module +//! +//! In-memory implementation of the [`FileSystem`](crate::FileSystem) trait for testing. +//! +//! ## What +//! +//! This module provides [`MockFileSystem`], an in-memory filesystem implementation +//! designed for unit and integration testing. It simulates a complete filesystem +//! without touching the disk, enabling fast, deterministic, and isolated tests. +//! +//! ## How +//! +//! `MockFileSystem` maintains an in-memory data structure (typically a `HashMap` or +//! tree structure) that represents the filesystem state: +//! - Files are stored as byte vectors with associated metadata +//! - Directories are represented as containers of entries +//! - Symlinks store their target paths +//! +//! The implementation supports: +//! - Pre-populating the filesystem with test data +//! - Simulating errors for specific paths (e.g., permission denied) +//! - Inspecting filesystem state after operations +//! +//! Unlike `RealFileSystem`, the mock does not enforce timeouts, as all operations +//! are instantaneous in-memory operations. +//! +//! ## Why +//! +//! A mock filesystem is essential for: +//! - **Speed**: No disk I/O means tests run in microseconds, not milliseconds +//! - **Determinism**: Tests produce the same results regardless of host filesystem state +//! - **Isolation**: Tests cannot interfere with each other or the host system +//! - **Error Simulation**: Can test error handling without complex setup +//! - **CI/CD Compatibility**: Works in containerized environments with read-only filesystems +//! +//! ## Example +//! +//! ```rust,ignore +//! use workspace_fs::{FileSystem, MockFileSystem}; +//! use std::path::Path; +//! +//! #[tokio::test] +//! async fn test_read_config() { +//! // Create mock filesystem with test data +//! let mock = MockFileSystem::new(); +//! mock.create_file( +//! Path::new("config.json"), +//! r#"{"name": "test-project"}"#, +//! ).await.unwrap(); +//! +//! // Test the function under test +//! let content = mock.read_to_string(Path::new("config.json")).await.unwrap(); +//! assert!(content.contains("test-project")); +//! } +//! +//! #[tokio::test] +//! async fn test_file_not_found() { +//! let mock = MockFileSystem::new(); +//! +//! // Test error handling +//! let result = mock.read_to_string(Path::new("nonexistent.json")).await; +//! assert!(result.is_err()); +//! } +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-0ea (MockFileSystem Implementation) +#![allow(clippy::todo)] diff --git a/crates/filesystem/src/path_ext.rs b/crates/filesystem/src/path_ext.rs new file mode 100644 index 0000000..b9af751 --- /dev/null +++ b/crates/filesystem/src/path_ext.rs @@ -0,0 +1,43 @@ +//! # Path Extension Module +//! +//! Provides utility extensions for working with filesystem paths. +//! +//! ## What +//! +//! This module defines the [`PathExt`] trait, which adds convenience methods to +//! [`std::path::Path`] for common path manipulation tasks. These are pure functions +//! that operate on path data without performing any I/O operations. +//! +//! ## How +//! +//! The trait is implemented for `Path` and provides methods such as: +//! - Path normalization (resolving `.` and `..` components) +//! - Cross-platform path handling +//! - Extension manipulation utilities +//! +//! All methods are synchronous because they operate only on in-memory path data, +//! not on the filesystem. This makes them safe to use in any context. +//! +//! ## Why +//! +//! Path extension utilities are valuable for: +//! - **Normalization**: Ensure consistent path representation across platforms +//! - **Safety**: Validate paths before filesystem operations +//! - **Convenience**: Common operations available as methods, not functions +//! - **Portability**: Abstract over platform-specific path separators +//! +//! ## Example +//! +//! ```rust,ignore +//! use workspace_fs::PathExt; +//! use std::path::Path; +//! +//! let path = Path::new("/foo/bar/../baz/./qux"); +//! +//! // Normalize path without touching filesystem +//! let normalized = path.normalize(); +//! assert_eq!(normalized, Path::new("/foo/baz/qux")); +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-60y (PathExt Module) +#![allow(clippy::todo)] diff --git a/crates/filesystem/src/real.rs b/crates/filesystem/src/real.rs new file mode 100644 index 0000000..5ed98c1 --- /dev/null +++ b/crates/filesystem/src/real.rs @@ -0,0 +1,58 @@ +//! # Real FileSystem Module +//! +//! Production implementation of the [`FileSystem`](crate::FileSystem) trait using `tokio::fs`. +//! +//! ## What +//! +//! This module provides [`RealFileSystem`], the production-ready filesystem implementation +//! that performs actual I/O operations on the host's filesystem. It wraps `tokio::fs` to +//! provide async filesystem access with configurable timeout support. +//! +//! ## How +//! +//! `RealFileSystem` implements the `FileSystem` trait by: +//! 1. Delegating each operation to the corresponding `tokio::fs` function +//! 2. Wrapping operations in `tokio::time::timeout` using configured durations +//! 3. Converting `std::io::Error` into the crate's `Error` type with path context +//! 4. Logging operations at appropriate levels (trace for entry, debug for results) +//! +//! The implementation is stateless (configuration is stored but not mutated), making it +//! safe to share across tasks via `Arc`. +//! +//! ## Why +//! +//! A dedicated real filesystem implementation provides: +//! - **Async I/O**: Non-blocking operations for better concurrency in large monorepos +//! - **Timeouts**: Prevent hanging on slow or unresponsive filesystems (e.g., network mounts) +//! - **Error Context**: Every error includes the path and operation that failed +//! - **Logging**: Integrated logging for debugging and observability +//! - **Testability**: Can be swapped for `MockFileSystem` in tests +//! +//! ## Example +//! +//! ```rust,ignore +//! use workspace_fs::{FileSystem, RealFileSystem, FileSystemConfig}; +//! use std::path::Path; +//! use std::time::Duration; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), workspace_fs::Error> { +//! // Default configuration with 30s read/write, 60s operation timeouts +//! let fs = RealFileSystem::new(); +//! +//! // Custom configuration +//! let config = FileSystemConfig::builder() +//! .with_read_timeout(Duration::from_secs(10)) +//! .build(); +//! let fs = RealFileSystem::with_config(config); +//! +//! // Use the filesystem +//! let content = fs.read_to_string(Path::new("package.json")).await?; +//! println!("Content: {}", content); +//! +//! Ok(()) +//! } +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-1gx (RealFileSystem Implementation) +#![allow(clippy::todo)] diff --git a/crates/filesystem/src/tests.rs b/crates/filesystem/src/tests.rs new file mode 100644 index 0000000..c3d5f70 --- /dev/null +++ b/crates/filesystem/src/tests.rs @@ -0,0 +1,89 @@ +//! # Tests Module +//! +//! Unit tests for all workspace-fs modules, organized by module. +//! +//! ## What +//! +//! This module contains the unit test suite for the workspace-fs crate. Tests are +//! organized into submodules that mirror the main crate structure, ensuring each +//! module's functionality is thoroughly validated. +//! +//! ## How +//! +//! Tests are structured following these conventions: +//! - Each source module has a corresponding test submodule (e.g., `mod error`, `mod config`) +//! - Tests use `#[tokio::test]` for async test functions +//! - The `MockFileSystem` is used for testing filesystem operations without disk I/O +//! - Tests follow the Arrange-Act-Assert pattern for clarity +//! +//! Test submodules: +//! - `error`: Tests for error type construction and display +//! - `config`: Tests for configuration builder and defaults +//! - `types`: Tests for `FileType`, `DirEntry`, and `Metadata` +//! - `path_ext`: Tests for path normalization and utilities +//! - `traits`: Tests for `FileSystem` trait behavior via mock +//! - `real`: Integration tests for `RealFileSystem` (uses tempdir) +//! - `mock`: Tests for `MockFileSystem` behavior +//! +//! ## Why +//! +//! A dedicated tests module provides: +//! - **Organization**: All unit tests in one place, separate from production code +//! - **Discoverability**: Easy to find and run tests for specific functionality +//! - **Maintainability**: Test code doesn't clutter implementation files +//! - **Coverage Tracking**: Clear mapping between modules and their tests +//! +//! ## Example +//! +//! ```rust,ignore +//! // Run all workspace-fs tests +//! cargo test -p workspace-fs +//! +//! // Run tests for a specific module +//! cargo test -p workspace-fs error +//! +//! // Run a specific test +//! cargo test -p workspace-fs test_read_error_display +//! ``` + +#[cfg(test)] +mod error { + //! Tests for the error module. + // TODO: will be implemented on epic workspace-node-tools-906 (Error Module) +} + +#[cfg(test)] +mod config { + //! Tests for the config module. + // TODO: will be implemented on epic workspace-node-tools-g2t (Configuration Module) +} + +#[cfg(test)] +mod types { + //! Tests for the types module. + // TODO: will be implemented on epic workspace-node-tools-3q8 (Types Module) +} + +#[cfg(test)] +mod path_ext { + //! Tests for the path_ext module. + // TODO: will be implemented on epic workspace-node-tools-60y (PathExt Module) +} + +#[cfg(test)] +mod traits { + //! Tests for the traits module. + // TODO: will be implemented on epic workspace-node-tools-hek (FileSystem Trait) +} + +#[cfg(test)] +mod real { + //! Tests for the real filesystem module. + // TODO: will be implemented on epic workspace-node-tools-1gx (RealFileSystem Implementation) +} + +#[cfg(test)] +mod mock { + //! Tests for the mock filesystem module. + // TODO: will be implemented on epic workspace-node-tools-0ea (MockFileSystem Implementation) +} diff --git a/crates/filesystem/src/traits.rs b/crates/filesystem/src/traits.rs new file mode 100644 index 0000000..77ca102 --- /dev/null +++ b/crates/filesystem/src/traits.rs @@ -0,0 +1,53 @@ +//! # Traits Module +//! +//! Defines the core [`FileSystem`] trait that abstracts all filesystem operations. +//! +//! ## What +//! +//! This module provides the `FileSystem` trait, which is the central abstraction of +//! the workspace-fs crate. The trait defines a complete set of asynchronous filesystem +//! operations that can be implemented by different backends. +//! +//! ## How +//! +//! The trait uses `async_trait` to enable async methods in traits, which Rust does not +//! natively support with object safety. Each method: +//! - Takes `&self` to allow shared access (implementations handle internal synchronization) +//! - Accepts paths as `impl AsRef` for ergonomic use with `&str`, `String`, `PathBuf`, etc. +//! - Returns `Result` using the crate's error type +//! +//! Two implementations are provided: +//! - [`RealFileSystem`](crate::RealFileSystem): Production implementation using `tokio::fs` +//! - [`MockFileSystem`](crate::MockFileSystem): In-memory implementation for testing +//! +//! ## Why +//! +//! A trait-based abstraction enables: +//! - **Dependency Injection**: Pass filesystem as a parameter, not a global +//! - **Testing**: Swap in `MockFileSystem` for fast, deterministic unit tests +//! - **Flexibility**: Could add other implementations (e.g., cached, logged, remote) +//! - **Decoupling**: Application code doesn't depend on specific filesystem implementation +//! +//! ## Example +//! +//! ```rust,ignore +//! use workspace_fs::{FileSystem, RealFileSystem, MockFileSystem}; +//! use std::path::Path; +//! +//! // Generic function works with any FileSystem implementation +//! async fn read_json(fs: &FS, path: &Path) -> Result { +//! fs.read_to_string(path).await +//! } +//! +//! // In production +//! let fs = RealFileSystem::new(); +//! let content = read_json(&fs, Path::new("config.json")).await?; +//! +//! // In tests +//! let mock = MockFileSystem::new(); +//! mock.write("config.json", r#"{"key": "value"}"#).await?; +//! let content = read_json(&mock, Path::new("config.json")).await?; +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-hek (FileSystem Trait) +#![allow(clippy::todo)] diff --git a/crates/filesystem/src/types.rs b/crates/filesystem/src/types.rs new file mode 100644 index 0000000..9aa91c9 --- /dev/null +++ b/crates/filesystem/src/types.rs @@ -0,0 +1,44 @@ +//! # Types Module +//! +//! Core data types for representing filesystem entries and their metadata. +//! +//! ## What +//! +//! 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) +//! +//! ## How +//! +//! These types abstract over platform-specific filesystem representations: +//! - `FileType` is a simple enum that normalizes the different ways OSes report types +//! - `DirEntry` wraps the essential information needed when traversing directories +//! - `Metadata` provides a platform-agnostic view of file attributes +//! +//! All types implement common traits (`Debug`, `Clone`, `PartialEq`, `Eq`) for +//! ergonomic use in tests and application code. +//! +//! ## Why +//! +//! Custom types (rather than re-exporting `std::fs` types) provide: +//! - **Testability**: `MockFileSystem` can construct these without touching disk +//! - **Consistency**: Same types work for both real and mock implementations +//! - **Extensibility**: Can add custom fields without breaking API +//! - **Documentation**: Clear documentation specific to this crate's semantics +//! +//! ## 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()) +//! } +//! ``` + +// TODO: will be implemented on epic workspace-node-tools-3q8 (Types Module) +#![allow(clippy::todo)]