anyfs_backend/
ext.rs

1//! # Extension Traits
2//!
3//! Convenience methods for filesystem backends.
4//!
5//! ## Overview
6//!
7//! [`FsExt`] provides commonly-needed utility methods that aren't part of
8//! the core trait hierarchy. These are implemented as default methods with
9//! blanket implementations, so any `Fs` backend gets them for free.
10//!
11//! ## Available Methods
12//!
13//! | Method | Description |
14//! |--------|-------------|
15//! | [`is_file`](FsExt::is_file) | Check if path is a regular file |
16//! | [`is_dir`](FsExt::is_dir) | Check if path is a directory |
17//!
18//! ## JSON Support (Feature-Gated)
19//!
20//! With the `serde` feature enabled, additional methods are available:
21//!
22//! | Method | Description |
23//! |--------|-------------|
24//! | `read_json` | Read and deserialize JSON file |
25//! | `write_json` | Serialize and write JSON file |
26//!
27//! Enable with:
28//! ```toml
29//! [dependencies]
30//! anyfs-backend = { version = "0.1", features = ["serde"] }
31//! ```
32
33use crate::{Fs, FsError};
34use std::path::Path;
35
36/// Extension methods for any filesystem backend.
37///
38/// Provides convenience methods not in the core traits but commonly needed.
39/// All methods have default implementations, so backends get them automatically.
40///
41/// # Example
42///
43/// ```rust
44/// use anyfs_backend::{Fs, FsExt, FsError};
45/// use std::path::Path;
46///
47/// fn check_paths<B: Fs>(backend: &B) -> Result<(), FsError> {
48///     // FsExt methods are available on any Fs backend
49///     if backend.is_file(Path::new("/config.json"))? {
50///         println!("Config exists!");
51///     }
52///     
53///     if backend.is_dir(Path::new("/data"))? {
54///         println!("Data directory exists!");
55///     }
56///     
57///     Ok(())
58/// }
59/// ```
60pub trait FsExt: Fs {
61    /// Check if the path points to a regular file.
62    ///
63    /// Returns `Ok(false)` if the path doesn't exist (not an error).
64    /// Returns `Err` only for actual I/O errors (permission denied, etc.).
65    ///
66    /// # Example
67    ///
68    /// ```rust
69    /// use anyfs_backend::{Fs, FsExt, FsError};
70    /// use std::path::Path;
71    ///
72    /// fn process_file<B: Fs>(backend: &B, path: &Path) -> Result<(), FsError> {
73    ///     if backend.is_file(path)? {
74    ///         let data = backend.read(path)?;
75    ///         // Process the file...
76    ///     }
77    ///     Ok(())
78    /// }
79    /// ```
80    fn is_file(&self, path: &Path) -> Result<bool, FsError> {
81        match self.metadata(path) {
82            Ok(m) => Ok(m.is_file()),
83            Err(FsError::NotFound { .. }) => Ok(false),
84            Err(e) => Err(e),
85        }
86    }
87
88    /// Check if the path points to a directory.
89    ///
90    /// Returns `Ok(false)` if the path doesn't exist (not an error).
91    /// Returns `Err` only for actual I/O errors (permission denied, etc.).
92    ///
93    /// # Example
94    ///
95    /// ```rust
96    /// use anyfs_backend::{Fs, FsExt, FsError};
97    /// use std::path::Path;
98    ///
99    /// fn ensure_dir<B: Fs>(backend: &B, path: &Path) -> Result<(), FsError> {
100    ///     if !backend.is_dir(path)? {
101    ///         backend.create_dir_all(path)?;
102    ///     }
103    ///     Ok(())
104    /// }
105    /// ```
106    fn is_dir(&self, path: &Path) -> Result<bool, FsError> {
107        match self.metadata(path) {
108            Ok(m) => Ok(m.is_dir()),
109            Err(FsError::NotFound { .. }) => Ok(false),
110            Err(e) => Err(e),
111        }
112    }
113
114    /// Check if the path points to a symbolic link.
115    ///
116    /// Returns `Ok(false)` if the path doesn't exist (not an error).
117    /// Returns `Err` only for actual I/O errors.
118    ///
119    /// # Note
120    ///
121    /// This method uses regular `metadata()` which follows symlinks.
122    /// For backends implementing [`FsLink`](crate::FsLink), consider using
123    /// `symlink_metadata()` for more accurate symlink detection.
124    ///
125    /// # Example
126    ///
127    /// ```rust
128    /// use anyfs_backend::{Fs, FsExt, FsError};
129    /// use std::path::Path;
130    ///
131    /// fn check_link<B: Fs>(backend: &B, path: &Path) -> Result<bool, FsError> {
132    ///     backend.is_symlink(path)
133    /// }
134    /// ```
135    fn is_symlink(&self, path: &Path) -> Result<bool, FsError> {
136        match self.metadata(path) {
137            Ok(m) => Ok(m.is_symlink()),
138            Err(FsError::NotFound { .. }) => Ok(false),
139            Err(e) => Err(e),
140        }
141    }
142
143    /// Get the size of a file in bytes.
144    ///
145    /// Convenience method that extracts just the size from metadata.
146    ///
147    /// # Errors
148    ///
149    /// Returns `FsError::NotFound` if the path doesn't exist.
150    ///
151    /// # Example
152    ///
153    /// ```rust
154    /// use anyfs_backend::{Fs, FsExt, FsError};
155    /// use std::path::Path;
156    ///
157    /// fn check_size<B: Fs>(backend: &B, path: &Path) -> Result<u64, FsError> {
158    ///     backend.file_size(path)
159    /// }
160    /// ```
161    fn file_size(&self, path: &Path) -> Result<u64, FsError> {
162        Ok(self.metadata(path)?.size)
163    }
164}
165
166// Blanket implementation - any Fs backend gets FsExt for free
167impl<B: Fs + ?Sized> FsExt for B {}
168
169// =============================================================================
170// JSON Support (Feature-Gated)
171// =============================================================================
172
173#[cfg(feature = "serde")]
174mod json {
175    use super::*;
176    use serde::{de::DeserializeOwned, Serialize};
177
178    /// JSON serialization extension methods.
179    ///
180    /// Available when the `serde` feature is enabled.
181    pub trait FsExtJson: Fs {
182        /// Read a file and deserialize it as JSON.
183        ///
184        /// # Type Parameters
185        ///
186        /// - `T`: The type to deserialize into (must implement `DeserializeOwned`)
187        ///
188        /// # Errors
189        ///
190        /// - `FsError::NotFound` — File doesn't exist
191        /// - `FsError::InvalidData` — File isn't valid UTF-8
192        /// - `FsError::Deserialization` — JSON parsing failed
193        ///
194        /// # Example
195        ///
196        /// ```rust
197        /// use anyfs_backend::{Fs, FsError};
198        /// #[cfg(feature = "serde")]
199        /// use anyfs_backend::FsExtJson;
200        /// use std::path::Path;
201        ///
202        /// #[cfg(feature = "serde")]
203        /// fn load_config<B: Fs>(backend: &B) -> Result<serde_json::Value, FsError> {
204        ///     backend.read_json(Path::new("/config.json"))
205        /// }
206        /// ```
207        fn read_json<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError> {
208            let data = self.read_to_string(path)?;
209            serde_json::from_str(&data).map_err(|e| FsError::Deserialization(e.to_string()))
210        }
211
212        /// Serialize a value and write it as JSON.
213        ///
214        /// Uses pretty-printing with 2-space indentation.
215        ///
216        /// # Type Parameters
217        ///
218        /// - `T`: The type to serialize (must implement `Serialize`)
219        ///
220        /// # Errors
221        ///
222        /// - `FsError::Serialization` — JSON serialization failed
223        /// - Other `FsError` variants from the underlying `write()` call
224        ///
225        /// # Example
226        ///
227        /// ```rust
228        /// use anyfs_backend::{Fs, FsError};
229        /// #[cfg(feature = "serde")]
230        /// use anyfs_backend::FsExtJson;
231        /// use std::path::Path;
232        ///
233        /// #[cfg(feature = "serde")]
234        /// fn save_value<B: Fs>(backend: &B, value: &serde_json::Value) -> Result<(), FsError> {
235        ///     backend.write_json(Path::new("/config.json"), value)
236        /// }
237        /// ```
238        fn write_json<T: Serialize>(&self, path: &Path, value: &T) -> Result<(), FsError> {
239            let json = serde_json::to_string_pretty(value)
240                .map_err(|e| FsError::Serialization(e.to_string()))?;
241            self.write(path, json.as_bytes())
242        }
243    }
244
245    // Blanket implementation
246    impl<B: Fs + ?Sized> FsExtJson for B {}
247}
248
249#[cfg(feature = "serde")]
250pub use json::FsExtJson;
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::{FsDir, FsRead, FsWrite, Metadata, ReadDirIter};
256    use std::io::{Read, Write};
257
258    /// Mock backend for testing
259    struct MockFs {
260        file_exists: bool,
261        dir_exists: bool,
262    }
263
264    impl MockFs {
265        fn with_file() -> Self {
266            Self {
267                file_exists: true,
268                dir_exists: false,
269            }
270        }
271
272        fn with_dir() -> Self {
273            Self {
274                file_exists: false,
275                dir_exists: true,
276            }
277        }
278
279        fn empty() -> Self {
280            Self {
281                file_exists: false,
282                dir_exists: false,
283            }
284        }
285    }
286
287    impl FsRead for MockFs {
288        fn read(&self, _: &Path) -> Result<Vec<u8>, FsError> {
289            Ok(vec![])
290        }
291
292        fn read_to_string(&self, _: &Path) -> Result<String, FsError> {
293            Ok(String::new())
294        }
295
296        fn read_range(&self, _: &Path, _: u64, _: usize) -> Result<Vec<u8>, FsError> {
297            Ok(vec![])
298        }
299
300        fn exists(&self, _: &Path) -> Result<bool, FsError> {
301            Ok(self.file_exists || self.dir_exists)
302        }
303
304        fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
305            if self.file_exists {
306                Ok(Metadata {
307                    file_type: crate::FileType::File,
308                    size: 100,
309                    ..Metadata::default()
310                })
311            } else if self.dir_exists {
312                Ok(Metadata {
313                    file_type: crate::FileType::Directory,
314                    size: 0,
315                    ..Metadata::default()
316                })
317            } else {
318                Err(FsError::NotFound {
319                    path: path.to_path_buf(),
320                })
321            }
322        }
323
324        fn open_read(&self, _: &Path) -> Result<Box<dyn Read + Send>, FsError> {
325            Ok(Box::new(std::io::empty()))
326        }
327    }
328
329    impl FsWrite for MockFs {
330        fn write(&self, _: &Path, _: &[u8]) -> Result<(), FsError> {
331            Ok(())
332        }
333
334        fn append(&self, _: &Path, _: &[u8]) -> Result<(), FsError> {
335            Ok(())
336        }
337
338        fn truncate(&self, _: &Path, _: u64) -> Result<(), FsError> {
339            Ok(())
340        }
341
342        fn remove_file(&self, _: &Path) -> Result<(), FsError> {
343            Ok(())
344        }
345
346        fn rename(&self, _: &Path, _: &Path) -> Result<(), FsError> {
347            Ok(())
348        }
349
350        fn copy(&self, _: &Path, _: &Path) -> Result<(), FsError> {
351            Ok(())
352        }
353
354        fn open_write(&self, _: &Path) -> Result<Box<dyn Write + Send>, FsError> {
355            Ok(Box::new(std::io::sink()))
356        }
357    }
358
359    impl FsDir for MockFs {
360        fn read_dir(&self, _: &Path) -> Result<ReadDirIter, FsError> {
361            Ok(ReadDirIter::from_vec(vec![]))
362        }
363
364        fn create_dir(&self, _: &Path) -> Result<(), FsError> {
365            Ok(())
366        }
367
368        fn create_dir_all(&self, _: &Path) -> Result<(), FsError> {
369            Ok(())
370        }
371
372        fn remove_dir(&self, _: &Path) -> Result<(), FsError> {
373            Ok(())
374        }
375
376        fn remove_dir_all(&self, _: &Path) -> Result<(), FsError> {
377            Ok(())
378        }
379    }
380
381    #[test]
382    fn is_file_returns_true_for_files() {
383        let fs = MockFs::with_file();
384        assert!(fs.is_file(Path::new("/test.txt")).unwrap());
385    }
386
387    #[test]
388    fn is_file_returns_false_for_dirs() {
389        let fs = MockFs::with_dir();
390        assert!(!fs.is_file(Path::new("/dir")).unwrap());
391    }
392
393    #[test]
394    fn is_file_returns_false_for_missing() {
395        let fs = MockFs::empty();
396        assert!(!fs.is_file(Path::new("/missing")).unwrap());
397    }
398
399    #[test]
400    fn is_dir_returns_true_for_dirs() {
401        let fs = MockFs::with_dir();
402        assert!(fs.is_dir(Path::new("/dir")).unwrap());
403    }
404
405    #[test]
406    fn is_dir_returns_false_for_files() {
407        let fs = MockFs::with_file();
408        assert!(!fs.is_dir(Path::new("/test.txt")).unwrap());
409    }
410
411    #[test]
412    fn is_dir_returns_false_for_missing() {
413        let fs = MockFs::empty();
414        assert!(!fs.is_dir(Path::new("/missing")).unwrap());
415    }
416
417    #[test]
418    fn file_size_returns_size() {
419        let fs = MockFs::with_file();
420        assert_eq!(fs.file_size(Path::new("/test.txt")).unwrap(), 100);
421    }
422
423    #[test]
424    fn file_size_errors_on_missing() {
425        let fs = MockFs::empty();
426        let result = fs.file_size(Path::new("/missing"));
427        assert!(matches!(result, Err(FsError::NotFound { .. })));
428    }
429
430    #[test]
431    fn fs_ext_available_on_dyn_fs() {
432        let fs: &dyn Fs = &MockFs::with_file();
433        // FsExt methods work on trait objects
434        assert!(fs.is_file(Path::new("/test.txt")).unwrap());
435    }
436}