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}