1use crate::formats::ImageFormat;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ImageMetadata {
11 pub path: PathBuf,
13 pub format: String,
15 pub size_bytes: u64,
17 pub width: u32,
19 pub height: u32,
21 pub hash: String,
23}
24
25impl ImageMetadata {
26 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 pub fn format_str(&self) -> &str {
47 &self.format
48 }
49
50 pub fn dimensions(&self) -> (u32, u32) {
52 (self.width, self.height)
53 }
54
55 pub fn size_mb(&self) -> f64 {
57 self.size_bytes as f64 / (1024.0 * 1024.0)
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ImageAnalysisResult {
64 pub image_hash: String,
66 pub analysis: String,
68 pub provider: String,
70 pub timestamp: DateTime<Utc>,
72 pub tokens_used: u32,
74}
75
76impl ImageAnalysisResult {
77 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ImageCacheEntry {
112 pub hash: String,
114 pub analysis: ImageAnalysisResult,
116 pub created_at: DateTime<Utc>,
118 pub expires_at: DateTime<Utc>,
120}
121
122impl ImageCacheEntry {
123 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 pub fn is_expired(&self) -> bool {
138 Utc::now() > self.expires_at
139 }
140
141 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 assert!(result.is_valid(86400));
195
196 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 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}