ricecoder_images/
models.rs

1//! Data models for image metadata, analysis results, and cache entries.
2
3use crate::formats::ImageFormat;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8/// Metadata about an image file.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ImageMetadata {
11    /// Path to the image file
12    pub path: PathBuf,
13    /// Image format (PNG, JPG, GIF, WebP)
14    pub format: String,
15    /// File size in bytes
16    pub size_bytes: u64,
17    /// Image width in pixels
18    pub width: u32,
19    /// Image height in pixels
20    pub height: u32,
21    /// SHA256 hash of the image file (used as cache key)
22    pub hash: String,
23}
24
25impl ImageMetadata {
26    /// Create a new image metadata struct.
27    pub fn new(
28        path: PathBuf,
29        format: ImageFormat,
30        size_bytes: u64,
31        width: u32,
32        height: u32,
33        hash: String,
34    ) -> Self {
35        Self {
36            path,
37            format: format.as_str().to_string(),
38            size_bytes,
39            width,
40            height,
41            hash,
42        }
43    }
44
45    /// Get the image format as a string.
46    pub fn format_str(&self) -> &str {
47        &self.format
48    }
49
50    /// Get the image dimensions as a tuple.
51    pub fn dimensions(&self) -> (u32, u32) {
52        (self.width, self.height)
53    }
54
55    /// Get the file size in MB.
56    pub fn size_mb(&self) -> f64 {
57        self.size_bytes as f64 / (1024.0 * 1024.0)
58    }
59}
60
61/// Result of image analysis by an AI provider.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ImageAnalysisResult {
64    /// SHA256 hash of the analyzed image
65    pub image_hash: String,
66    /// Analysis text from the provider
67    pub analysis: String,
68    /// Name of the provider that performed the analysis
69    pub provider: String,
70    /// Timestamp when the analysis was performed
71    pub timestamp: DateTime<Utc>,
72    /// Number of tokens used for the analysis
73    pub tokens_used: u32,
74}
75
76impl ImageAnalysisResult {
77    /// Create a new image analysis result.
78    pub fn new(
79        image_hash: String,
80        analysis: String,
81        provider: String,
82        tokens_used: u32,
83    ) -> Self {
84        Self {
85            image_hash,
86            analysis,
87            provider,
88            timestamp: Utc::now(),
89            tokens_used,
90        }
91    }
92
93    /// Check if the analysis result is still valid (not expired).
94    ///
95    /// # Arguments
96    ///
97    /// * `ttl_seconds` - Time-to-live in seconds
98    ///
99    /// # Returns
100    ///
101    /// True if the result is still valid, false if expired
102    pub fn is_valid(&self, ttl_seconds: u64) -> bool {
103        let now = Utc::now();
104        let age = now.signed_duration_since(self.timestamp);
105        age.num_seconds() < ttl_seconds as i64
106    }
107}
108
109/// Cache entry for image analysis results.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ImageCacheEntry {
112    /// SHA256 hash of the image (cache key)
113    pub hash: String,
114    /// The cached analysis result
115    pub analysis: ImageAnalysisResult,
116    /// When the cache entry was created
117    pub created_at: DateTime<Utc>,
118    /// When the cache entry expires
119    pub expires_at: DateTime<Utc>,
120}
121
122impl ImageCacheEntry {
123    /// Create a new cache entry.
124    pub fn new(hash: String, analysis: ImageAnalysisResult, ttl_seconds: u64) -> Self {
125        let now = Utc::now();
126        let expires_at = now + chrono::Duration::seconds(ttl_seconds as i64);
127
128        Self {
129            hash,
130            analysis,
131            created_at: now,
132            expires_at,
133        }
134    }
135
136    /// Check if the cache entry has expired.
137    pub fn is_expired(&self) -> bool {
138        Utc::now() > self.expires_at
139    }
140
141    /// Get the remaining TTL in seconds.
142    pub fn remaining_ttl_seconds(&self) -> i64 {
143        let remaining = self.expires_at.signed_duration_since(Utc::now());
144        remaining.num_seconds().max(0)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_image_metadata_creation() {
154        let metadata = ImageMetadata::new(
155            PathBuf::from("/path/to/image.png"),
156            ImageFormat::Png,
157            1024 * 1024,
158            800,
159            600,
160            "abc123".to_string(),
161        );
162
163        assert_eq!(metadata.format_str(), "png");
164        assert_eq!(metadata.dimensions(), (800, 600));
165        assert_eq!(metadata.size_bytes, 1024 * 1024);
166        assert!(metadata.size_mb() > 0.9 && metadata.size_mb() < 1.1);
167    }
168
169    #[test]
170    fn test_image_analysis_result_creation() {
171        let result = ImageAnalysisResult::new(
172            "hash123".to_string(),
173            "This is an image of a cat".to_string(),
174            "openai".to_string(),
175            100,
176        );
177
178        assert_eq!(result.image_hash, "hash123");
179        assert_eq!(result.analysis, "This is an image of a cat");
180        assert_eq!(result.provider, "openai");
181        assert_eq!(result.tokens_used, 100);
182    }
183
184    #[test]
185    fn test_image_analysis_result_validity() {
186        let result = ImageAnalysisResult::new(
187            "hash123".to_string(),
188            "Analysis".to_string(),
189            "openai".to_string(),
190            100,
191        );
192
193        // Should be valid with large TTL
194        assert!(result.is_valid(86400));
195
196        // Should be invalid with very small TTL
197        assert!(!result.is_valid(0));
198    }
199
200    #[test]
201    fn test_cache_entry_creation() {
202        let analysis = ImageAnalysisResult::new(
203            "hash123".to_string(),
204            "Analysis".to_string(),
205            "openai".to_string(),
206            100,
207        );
208
209        let entry = ImageCacheEntry::new("hash123".to_string(), analysis, 3600);
210
211        assert_eq!(entry.hash, "hash123");
212        assert!(!entry.is_expired());
213        assert!(entry.remaining_ttl_seconds() > 0);
214    }
215
216    #[test]
217    fn test_cache_entry_expiration() {
218        let analysis = ImageAnalysisResult::new(
219            "hash123".to_string(),
220            "Analysis".to_string(),
221            "openai".to_string(),
222            100,
223        );
224
225        // Create entry with 0 TTL (already expired)
226        let entry = ImageCacheEntry::new("hash123".to_string(), analysis, 0);
227
228        assert!(entry.is_expired());
229        assert_eq!(entry.remaining_ttl_seconds(), 0);
230    }
231}