Skip to main content

cache_lite/
lib.rs

1/*
2 * @filename: lib.rs
3 * @description: A cross-platform caching library for Rust with configurable storage, lifecycle, and file formatting.
4 * @author: TaimWay <taimway@gmail.com>
5 * 
6 * Copyright (C) 2026 TaimWay
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a copy
9 * of this software and associated documentation files (the "Software"), to deal
10 * in the Software without restriction, including without limitation the rights
11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 * copies of the Software, and to permit persons to whom the Software is
13 * furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in all
16 * copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 * SOFTWARE.
25 */
26
27//! # Rust Cache Library
28//! 
29//! A lightweight, cross-platform caching library for Rust applications.
30//! Provides configurable cache storage, lifecycle management, and file formatting.
31//! 
32//! # Features
33//! 
34//! - **Cross-platform**: Automatic path handling for Windows and Linux
35//! - **Configurable**: JSON-based configuration with runtime overrides
36//! - **Simple API**: Easy-to-use interface for cache operations
37//! - **File-based**: Persistent cache storage on disk
38//! 
39//! # Quick Start
40//! 
41//! ```no_run
42//! use cache_lite::{Cache, CacheConfig};
43//!
44//! fn main() -> Result<(), Box<dyn std::error::Error>> {
45//!     // Create cache with default configuration
46//!     let config = CacheConfig::default();
47//!     let mut cache = Cache::new(config)?;
48//!
49//!     // Create a new cache object
50//!     let cache_obj = cache.create("my_cache", None).unwrap();
51//!
52//!     // Write data to cache
53//!     cache_obj.write_string("Cached data").unwrap();
54//!
55//!     // Read data from cache
56//!     let data = cache_obj.get_string().unwrap();
57//!     assert_eq!(data, "Cached data");
58//!
59//!     cache_obj.delete()?;
60//!
61//!     Ok(())
62//! }
63//! ```
64//! 
65//! # Configuration
66//! 
67//! The library supports JSON configuration for customizing cache behavior:
68//! 
69//! ```json
70//! {
71//!   "path": {
72//!     "windows": "%temp%/MyApp/Cache",
73//!     "linux": "/tmp/myapp/cache"
74//!   },
75//!   "format": {
76//!     "filename": "{name}_{time}.cache",
77//!     "time": "%Y%m%d_%H%M%S"
78//!   }
79//! }
80//! ```
81//! 
82//! # Error Handling
83//! 
84//! The library provides a comprehensive error type `CacheError` for handling
85//! various failure scenarios including I/O errors, invalid configurations,
86//! permission issues, and more.
87
88mod config;
89mod object;
90mod cache;
91mod error;
92mod utils;
93
94// Re-export public API
95pub use config::{CacheConfig, CachePathConfig, CacheFormatConfig};
96pub use object::CacheObject;
97pub use cache::Cache;
98pub use error::CacheError;
99
100/// Result type alias for cache operations
101pub type CacheResult<T> = std::result::Result<T, CacheError>;
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use tempfile::tempdir;
107
108    #[test]
109    fn test_cache_config_default() {
110        let config = CacheConfig::default();
111        assert_eq!(config.max_size, 0);
112        assert_eq!(config.max_files, 0);
113        assert!(!config.path.windows.is_empty());
114        assert!(!config.path.linux.is_empty());
115        assert!(!config.format.filename.is_empty());
116        assert!(!config.format.time.is_empty());
117    }
118
119    #[test]
120    fn test_cache_config_from_json() {
121        let json = r#"{
122            "path": {
123                "windows": "%temp%/TestCache",
124                "linux": "/tmp/testcache"
125            },
126            "format": {
127                "filename": "test_{name}.cache",
128                "time": "%Y%m%d"
129            },
130            "max_size": 1024,
131            "max_files": 10
132        }"#;
133
134        let config = CacheConfig::new(json).unwrap();
135        assert_eq!(config.path.windows, "%temp%/TestCache");
136        assert_eq!(config.path.linux, "/tmp/testcache");
137        assert_eq!(config.format.filename, "test_{name}.cache");
138        assert_eq!(config.format.time, "%Y%m%d");
139        assert_eq!(config.max_size, 1024);
140        assert_eq!(config.max_files, 10);
141    }
142
143    #[test]
144    fn test_cache_config_from_partial_json() {
145        let json = r#"{
146            "path": {
147                "linux": "/custom/path"
148            },
149            "format": {
150                "filename": "custom_{name}.cache"
151            }
152        }"#;
153
154        let config = CacheConfig::new(json).unwrap();
155        assert_eq!(config.path.linux, "/custom/path");
156        assert_eq!(config.format.filename, "custom_{name}.cache");
157        // Windows path should use default
158        assert!(!config.path.windows.is_empty());
159        // Time format should use default
160        assert!(!config.format.time.is_empty());
161    }
162
163    #[test]
164    fn test_cache_config_new_or_default() {
165        let json = "invalid json";
166        let config = CacheConfig::new_or_default(json);
167        // Should fall back to default
168        assert_eq!(config.max_size, 0);
169        assert_eq!(config.max_files, 0);
170    }
171
172    #[test]
173    fn test_cache_creation() {
174        let temp_dir = tempdir().unwrap();
175        let config_json = format!(
176            r#"{{
177                "path": {{
178                    "windows": "{}",
179                    "linux": "{}"
180                }},
181                "format": {{
182                    "filename": "{{name}}.cache",
183                    "time": "%Y%m%d"
184                }},
185                "max_size": 0,
186                "max_files": 0
187            }}"#,
188            temp_dir.path().to_string_lossy(),
189            temp_dir.path().to_string_lossy()
190        );
191
192        let config = CacheConfig::new(&config_json).unwrap();
193        let mut cache = Cache::new(config).unwrap();
194
195        // Test create cache object
196        let cache_obj = cache.create("test_cache", None).unwrap();
197        assert_eq!(cache_obj.name(), "test_cache");
198        
199        // Write some data to ensure file exists
200        cache_obj.write_string("test data").unwrap();
201        assert!(cache_obj.exists());
202
203        // Test duplicate creation fails
204        let result = cache.create("test_cache", None);
205        assert!(result.is_err());
206        if let Err(e) = result {
207            assert!(matches!(e, CacheError::AlreadyExists(_)));
208        }
209
210        // Test get cache object
211        let retrieved = cache.get("test_cache").unwrap();
212        assert_eq!(retrieved.name(), "test_cache");
213        assert_eq!(retrieved.id(), cache_obj.id());
214
215        // Test get non-existent cache
216        let result = cache.get("nonexistent");
217        assert!(result.is_err());
218        if let Err(e) = result {
219            assert!(matches!(e, CacheError::NotFound(_)));
220        }
221
222        // Test length and empty
223        assert_eq!(cache.len(), 1);
224        assert!(!cache.is_empty());
225
226        // Test iterator
227        let objects: Vec<_> = cache.iter().collect();
228        assert_eq!(objects.len(), 1);
229        assert_eq!(objects[0].name(), "test_cache");
230    }
231
232    #[test]
233    fn test_cache_operations() {
234        let temp_dir = tempdir().unwrap();
235        let config_json = format!(
236            r#"{{
237                "path": {{
238                    "windows": "{}",
239                    "linux": "{}"
240                }},
241                "format": {{
242                    "filename": "{{name}}.cache",
243                    "time": "%Y%m%d"
244                }},
245                "max_size": 0,
246                "max_files": 0
247            }}"#,
248            temp_dir.path().to_string_lossy(),
249            temp_dir.path().to_string_lossy()
250        );
251
252        let config = CacheConfig::new(&config_json).unwrap();
253        let mut cache = Cache::new(config).unwrap();
254
255        // Create multiple cache objects
256        cache.create("cache1", None).unwrap();
257        cache.create("cache2", None).unwrap();
258        cache.create("cache3", None).unwrap();
259
260        assert_eq!(cache.len(), 3);
261
262        // Test remove operation
263        cache.remove("cache2").unwrap();
264        assert_eq!(cache.len(), 2);
265        assert!(cache.get("cache2").is_err());
266
267        // Test clear operation
268        cache.clear().unwrap();
269        assert_eq!(cache.len(), 0);
270        assert!(cache.is_empty());
271    }
272
273    #[test]
274    fn test_cache_object_operations() {
275        let temp_dir = tempdir().unwrap();
276        let config_json = format!(
277            r#"{{
278                "path": {{
279                    "windows": "{}",
280                    "linux": "{}"
281                }},
282                "format": {{
283                    "filename": "{{name}}.cache",
284                    "time": "%Y%m%d"
285                }},
286                "max_size": 0,
287                "max_files": 0
288            }}"#,
289            temp_dir.path().to_string_lossy(),
290            temp_dir.path().to_string_lossy()
291        );
292
293        let config = CacheConfig::new(&config_json).unwrap();
294        let mut cache = Cache::new(config).unwrap();
295        let cache_obj = cache.create("test_operations", None).unwrap();
296
297        // Test string operations
298        let test_string = "Hello, World!";
299        cache_obj.write_string(test_string).unwrap();
300
301        let read_string = cache_obj.get_string().unwrap();
302        assert_eq!(read_string, test_string);
303
304        // Test binary operations
305        let test_bytes = vec![1, 2, 3, 4, 5];
306        cache_obj.write_bytes(&test_bytes).unwrap();
307
308        let read_bytes = cache_obj.get_bytes().unwrap();
309        assert_eq!(read_bytes, test_bytes);
310
311        // Test file operations
312        let file = cache_obj.get_file().unwrap();
313        assert!(file.metadata().is_ok());
314
315        // Test size
316        let size = cache_obj.size().unwrap();
317        assert!(size > 0);
318
319        // Test delete
320        cache_obj.delete().unwrap();
321        assert!(!cache_obj.exists());
322
323        // Test creation time
324        let new_obj = cache.create("new_cache", None).unwrap();
325        let created_at = new_obj.created_at();
326        assert!(created_at.elapsed().is_ok());
327    }
328
329    #[test]
330    fn test_cache_with_custom_config() {
331        let temp_dir = tempdir().unwrap();
332        let base_config_json = format!(
333            r#"{{
334                "path": {{
335                    "windows": "{}",
336                    "linux": "{}"
337                }},
338                "format": {{
339                    "filename": "base_{{name}}.cache",
340                    "time": "%Y%m%d"
341                }},
342                "max_size": 0,
343                "max_files": 0
344            }}"#,
345            temp_dir.path().to_string_lossy(),
346            temp_dir.path().to_string_lossy()
347        );
348
349        let base_config = CacheConfig::new(&base_config_json).unwrap();
350        let mut cache = Cache::new(base_config).unwrap();
351
352        let custom_config = r#"{
353            "path": {
354                "linux": "/custom/path"
355            },
356            "format": {
357                "filename": "custom_{name}_{id}.cache"
358            }
359        }"#;
360
361        let cache_obj = cache.create("custom_cache", Some(custom_config)).unwrap();
362        let path_str = cache_obj.path().to_string_lossy().to_string();
363
364        // Write data to ensure file exists
365        cache_obj.write_string("test").unwrap();
366
367        // Check that custom format is used
368        assert!(path_str.contains("custom_cache"));
369        assert!(path_str.contains(".cache"));
370    }
371
372    #[test]
373    fn test_cache_config_updates() {
374        let config = CacheConfig::default();
375        let mut cache = Cache::new(config).unwrap();
376
377        let new_config_json = r#"{
378            "path": {
379                "windows": "%temp%/UpdatedCache",
380                "linux": "/tmp/updatedcache"
381            },
382            "format": {
383                "filename": "updated_{name}.cache",
384                "time": "%H%M%S"
385            },
386            "max_size": 2048,
387            "max_files": 20
388        }"#;
389
390        let new_config = CacheConfig::new(new_config_json).unwrap();
391        cache.set_config(new_config.clone());
392
393        let retrieved_config = cache.get_config();
394        assert_eq!(retrieved_config.max_size, 2048);
395        assert_eq!(retrieved_config.max_files, 20);
396        assert_eq!(retrieved_config.format.filename, "updated_{name}.cache");
397    }
398
399    #[test]
400    fn test_validate_name() {
401        // Valid names
402        assert!(crate::utils::validate_name("valid_name").is_ok());
403        assert!(crate::utils::validate_name("valid123").is_ok());
404        assert!(crate::utils::validate_name("a").is_ok());
405
406        // Invalid names
407        assert!(crate::utils::validate_name("").is_err());
408        assert!(crate::utils::validate_name(&"a".repeat(256)).is_err());
409        assert!(crate::utils::validate_name("test/name").is_err());
410        assert!(crate::utils::validate_name("test\\name").is_err());
411        assert!(crate::utils::validate_name("test..name").is_err());
412        
413        #[cfg(windows)]
414        {
415            assert!(crate::utils::validate_name("CON").is_err());
416            assert!(crate::utils::validate_name("test:name").is_err());
417            assert!(crate::utils::validate_name("test<name").is_err());
418        }
419    }
420
421    #[test]
422    fn test_error_handling() {
423        // Test error creation
424        let io_error = CacheError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
425        assert_eq!(io_error.kind(), "io");
426
427        let generic_error = CacheError::new("Test error");
428        assert_eq!(generic_error.kind(), "generic");
429        assert_eq!(generic_error.message(), "Test error");
430
431        // Test error conversions
432        let io_err: std::io::Error = std::io::Error::new(std::io::ErrorKind::Other, "test");
433        let cache_err: CacheError = io_err.into();
434        assert!(cache_err.is_io_error());
435
436        let json_err = serde_json::from_str::<CacheConfig>("invalid json");
437        assert!(json_err.is_err());
438    }
439
440    #[test]
441    fn test_cache_object_clone() {
442        let temp_dir = tempdir().unwrap();
443        let config_json = format!(
444            r#"{{
445                "path": {{
446                    "windows": "{}",
447                    "linux": "{}"
448                }},
449                "format": {{
450                    "filename": "{{name}}.cache",
451                    "time": "%Y%m%d"
452                }},
453                "max_size": 0,
454                "max_files": 0
455            }}"#,
456            temp_dir.path().to_string_lossy(),
457            temp_dir.path().to_string_lossy()
458        );
459
460        let config = CacheConfig::new(&config_json).unwrap();
461        let mut cache = Cache::new(config).unwrap();
462        let cache_obj = cache.create("clone_test", None).unwrap();
463
464        // Write data first
465        cache_obj.write_string("test data").unwrap();
466
467        // Test cloning
468        let cloned = cache_obj.clone();
469        assert_eq!(cloned.name(), cache_obj.name());
470        assert_eq!(cloned.id(), cache_obj.id());
471        assert_eq!(cloned.path(), cache_obj.path());
472
473        // Check clone sees the same content
474        let cloned_content = cloned.get_string().unwrap();
475        assert_eq!(cloned_content, "test data");
476    }
477
478    #[test]
479    fn test_expand_path() {
480        // Test tilde expansion
481        let path_with_tilde = "~/test/path";
482        let expanded = crate::utils::expand_path(path_with_tilde);
483        if let Some(home) = dirs::home_dir() {
484            let home_str = home.to_string_lossy();
485            assert!(expanded.starts_with(&*home_str));
486        }
487
488        // Test Windows env var expansion (only on Windows)
489        #[cfg(windows)]
490        {
491            let path_with_env = "%temp%/test";
492            let expanded = crate::utils::expand_path(path_with_env);
493            assert!(!expanded.contains("%temp%"));
494        }
495
496        // Test path separator conversion
497        let unix_path = "path/to/file";
498        let expanded = crate::utils::expand_path(unix_path);
499        
500        #[cfg(windows)]
501        assert!(expanded.contains('\\'));
502        
503        #[cfg(unix)]
504        assert!(expanded.contains('/'));
505    }
506
507    #[test]
508    fn test_cache_clear_with_errors() {
509        let temp_dir = tempdir().unwrap();
510        let config_json = format!(
511            r#"{{
512                "path": {{
513                    "windows": "{}",
514                    "linux": "{}"
515                }},
516                "format": {{
517                    "filename": "{{name}}.cache",
518                    "time": "%Y%m%d"
519                }},
520                "max_size": 0,
521                "max_files": 0
522            }}"#,
523            temp_dir.path().to_string_lossy(),
524            temp_dir.path().to_string_lossy()
525        );
526
527        let config = CacheConfig::new(&config_json).unwrap();
528        let mut cache = Cache::new(config).unwrap();
529
530        // Create a cache object
531        let cache_obj = cache.create("test_clear", None).unwrap();
532        
533        // Write data to ensure file exists
534        cache_obj.write_string("test data").unwrap();
535        assert!(cache_obj.exists());
536
537        // Clear should work
538        cache.clear().unwrap();
539        
540        // Cache should be empty
541        assert_eq!(cache.len(), 0);
542        assert!(cache.is_empty());
543        
544        // File should be deleted
545        assert!(!cache_obj.exists());
546    }
547
548    #[test]
549    fn test_error_matches() {
550        // Test error matches
551        let io_error = CacheError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
552        assert!(io_error.is_io_error());
553
554        let not_found_error = CacheError::NotFound("test".to_string());
555        assert!(not_found_error.is_not_found());
556
557        let permission_error = CacheError::PermissionDenied("test".to_string());
558        assert!(permission_error.is_permission_denied());
559    }
560
561    #[test]
562    fn test_config_serde_roundtrip() {
563        let config = CacheConfig::default();
564        let json = serde_json::to_string(&config).unwrap();
565        let parsed_config = CacheConfig::new(&json).unwrap();
566        
567        assert_eq!(config.max_size, parsed_config.max_size);
568        assert_eq!(config.max_files, parsed_config.max_files);
569        assert_eq!(config.path.windows, parsed_config.path.windows);
570        assert_eq!(config.path.linux, parsed_config.path.linux);
571        assert_eq!(config.format.filename, parsed_config.format.filename);
572        assert_eq!(config.format.time, parsed_config.format.time);
573    }
574}