ricecoder_images/
session_integration.rs

1//! Session integration for storing and retrieving images in session history and context.
2//!
3//! This module provides functionality to:
4//! - Store images in message history with metadata
5//! - Persist image metadata with sessions
6//! - Store image references in session context
7//! - Support image sharing in sessions
8
9use crate::models::{ImageAnalysisResult, ImageMetadata};
10use crate::error::{ImageError, ImageResult};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// Metadata about an image included in a message
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct MessageImageMetadata {
17    /// SHA256 hash of the image (unique identifier)
18    pub hash: String,
19    /// Image metadata (path, format, size, dimensions)
20    pub metadata: ImageMetadata,
21    /// Analysis result if available
22    pub analysis: Option<ImageAnalysisResult>,
23    /// Whether the image was cached
24    pub was_cached: bool,
25}
26
27impl MessageImageMetadata {
28    /// Create new message image metadata
29    pub fn new(
30        hash: String,
31        metadata: ImageMetadata,
32        analysis: Option<ImageAnalysisResult>,
33        was_cached: bool,
34    ) -> Self {
35        Self {
36            hash,
37            metadata,
38            analysis,
39            was_cached,
40        }
41    }
42}
43
44/// Images included in a message
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
46pub struct MessageImages {
47    /// Map of image hash to image metadata
48    images: HashMap<String, MessageImageMetadata>,
49}
50
51impl MessageImages {
52    /// Create a new empty message images collection
53    pub fn new() -> Self {
54        Self {
55            images: HashMap::new(),
56        }
57    }
58
59    /// Add an image to the message
60    ///
61    /// # Arguments
62    ///
63    /// * `image_meta` - The image metadata to add
64    ///
65    /// # Returns
66    ///
67    /// Ok if the image was added, Err if an image with the same hash already exists
68    pub fn add_image(&mut self, image_meta: MessageImageMetadata) -> ImageResult<()> {
69        if self.images.contains_key(&image_meta.hash) {
70            return Err(ImageError::InvalidFile(
71                format!("Image with hash {} already exists in message", image_meta.hash),
72            ));
73        }
74
75        self.images.insert(image_meta.hash.clone(), image_meta);
76        Ok(())
77    }
78
79    /// Get an image by hash
80    pub fn get_image(&self, hash: &str) -> Option<&MessageImageMetadata> {
81        self.images.get(hash)
82    }
83
84    /// Get all images in the message
85    pub fn get_all_images(&self) -> Vec<&MessageImageMetadata> {
86        self.images.values().collect()
87    }
88
89    /// Get the number of images in the message
90    pub fn image_count(&self) -> usize {
91        self.images.len()
92    }
93
94    /// Check if the message has any images
95    pub fn has_images(&self) -> bool {
96        !self.images.is_empty()
97    }
98
99    /// Remove an image by hash
100    pub fn remove_image(&mut self, hash: &str) -> Option<MessageImageMetadata> {
101        self.images.remove(hash)
102    }
103
104    /// Get all image hashes
105    pub fn get_image_hashes(&self) -> Vec<String> {
106        self.images.keys().cloned().collect()
107    }
108
109    /// Get all images as a vector
110    pub fn to_vec(&self) -> Vec<MessageImageMetadata> {
111        self.images.values().cloned().collect()
112    }
113
114    /// Clear all images from the message
115    pub fn clear(&mut self) {
116        self.images.clear();
117    }
118}
119
120/// Session context for images
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122pub struct SessionImageContext {
123    /// Images currently in the session context (by hash)
124    pub current_images: Vec<String>,
125    /// All images ever included in this session (by hash)
126    pub all_images: Vec<String>,
127    /// Map of image hash to metadata for quick lookup
128    pub image_metadata: HashMap<String, ImageMetadata>,
129}
130
131impl SessionImageContext {
132    /// Create a new session image context
133    pub fn new() -> Self {
134        Self {
135            current_images: Vec::new(),
136            all_images: Vec::new(),
137            image_metadata: HashMap::new(),
138        }
139    }
140
141    /// Add an image to the current session context
142    ///
143    /// # Arguments
144    ///
145    /// * `hash` - The image hash
146    /// * `metadata` - The image metadata
147    pub fn add_image(&mut self, hash: String, metadata: ImageMetadata) {
148        if !self.current_images.contains(&hash) {
149            self.current_images.push(hash.clone());
150        }
151
152        if !self.all_images.contains(&hash) {
153            self.all_images.push(hash.clone());
154        }
155
156        self.image_metadata.insert(hash, metadata);
157    }
158
159    /// Remove an image from the current session context
160    ///
161    /// Note: The image is removed from current context but remains in all_images
162    pub fn remove_image(&mut self, hash: &str) {
163        self.current_images.retain(|h| h != hash);
164    }
165
166    /// Get the current images in the session
167    pub fn get_current_images(&self) -> Vec<&ImageMetadata> {
168        self.current_images
169            .iter()
170            .filter_map(|hash| self.image_metadata.get(hash))
171            .collect()
172    }
173
174    /// Get all images ever included in the session
175    pub fn get_all_images(&self) -> Vec<&ImageMetadata> {
176        self.all_images
177            .iter()
178            .filter_map(|hash| self.image_metadata.get(hash))
179            .collect()
180    }
181
182    /// Get image metadata by hash
183    pub fn get_image_metadata(&self, hash: &str) -> Option<&ImageMetadata> {
184        self.image_metadata.get(hash)
185    }
186
187    /// Check if an image is in the current context
188    pub fn has_image(&self, hash: &str) -> bool {
189        self.current_images.iter().any(|h| h == hash)
190    }
191
192    /// Get the number of current images
193    pub fn current_image_count(&self) -> usize {
194        self.current_images.len()
195    }
196
197    /// Get the total number of images ever included
198    pub fn total_image_count(&self) -> usize {
199        self.all_images.len()
200    }
201
202    /// Clear current images (but keep history)
203    pub fn clear_current(&mut self) {
204        self.current_images.clear();
205    }
206
207    /// Clear all images including history
208    pub fn clear_all(&mut self) {
209        self.current_images.clear();
210        self.all_images.clear();
211        self.image_metadata.clear();
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::path::PathBuf;
219
220    fn create_test_image_metadata() -> ImageMetadata {
221        ImageMetadata::new(
222            PathBuf::from("/path/to/image.png"),
223            crate::formats::ImageFormat::Png,
224            1024 * 1024,
225            800,
226            600,
227            "test_hash_123".to_string(),
228        )
229    }
230
231    fn create_test_message_image_metadata() -> MessageImageMetadata {
232        MessageImageMetadata::new(
233            "test_hash_123".to_string(),
234            create_test_image_metadata(),
235            None,
236            false,
237        )
238    }
239
240    #[test]
241    fn test_message_images_add() {
242        let mut msg_images = MessageImages::new();
243        let image_meta = create_test_message_image_metadata();
244
245        assert!(msg_images.add_image(image_meta.clone()).is_ok());
246        assert_eq!(msg_images.image_count(), 1);
247        assert!(msg_images.has_images());
248    }
249
250    #[test]
251    fn test_message_images_duplicate() {
252        let mut msg_images = MessageImages::new();
253        let image_meta = create_test_message_image_metadata();
254
255        assert!(msg_images.add_image(image_meta.clone()).is_ok());
256        assert!(msg_images.add_image(image_meta).is_err());
257        assert_eq!(msg_images.image_count(), 1);
258    }
259
260    #[test]
261    fn test_message_images_get() {
262        let mut msg_images = MessageImages::new();
263        let image_meta = create_test_message_image_metadata();
264
265        msg_images.add_image(image_meta.clone()).unwrap();
266
267        let retrieved = msg_images.get_image("test_hash_123");
268        assert!(retrieved.is_some());
269        assert_eq!(retrieved.unwrap().hash, "test_hash_123");
270    }
271
272    #[test]
273    fn test_message_images_remove() {
274        let mut msg_images = MessageImages::new();
275        let image_meta = create_test_message_image_metadata();
276
277        msg_images.add_image(image_meta).unwrap();
278        assert_eq!(msg_images.image_count(), 1);
279
280        let removed = msg_images.remove_image("test_hash_123");
281        assert!(removed.is_some());
282        assert_eq!(msg_images.image_count(), 0);
283    }
284
285    #[test]
286    fn test_message_images_get_all() {
287        let mut msg_images = MessageImages::new();
288
289        for i in 0..3 {
290            let mut image_meta = create_test_message_image_metadata();
291            image_meta.hash = format!("hash_{}", i);
292            msg_images.add_image(image_meta).unwrap();
293        }
294
295        let all = msg_images.get_all_images();
296        assert_eq!(all.len(), 3);
297    }
298
299    #[test]
300    fn test_session_image_context_add() {
301        let mut ctx = SessionImageContext::new();
302        let metadata = create_test_image_metadata();
303
304        ctx.add_image("hash1".to_string(), metadata);
305
306        assert_eq!(ctx.current_image_count(), 1);
307        assert_eq!(ctx.total_image_count(), 1);
308        assert!(ctx.has_image("hash1"));
309    }
310
311    #[test]
312    fn test_session_image_context_remove() {
313        let mut ctx = SessionImageContext::new();
314        let metadata = create_test_image_metadata();
315
316        ctx.add_image("hash1".to_string(), metadata);
317        assert_eq!(ctx.current_image_count(), 1);
318
319        ctx.remove_image("hash1");
320        assert_eq!(ctx.current_image_count(), 0);
321        assert_eq!(ctx.total_image_count(), 1); // Still in history
322    }
323
324    #[test]
325    fn test_session_image_context_history() {
326        let mut ctx = SessionImageContext::new();
327        let metadata = create_test_image_metadata();
328
329        ctx.add_image("hash1".to_string(), metadata.clone());
330        ctx.add_image("hash2".to_string(), metadata.clone());
331
332        ctx.remove_image("hash1");
333
334        assert_eq!(ctx.current_image_count(), 1);
335        assert_eq!(ctx.total_image_count(), 2);
336        assert!(!ctx.has_image("hash1"));
337        assert!(ctx.has_image("hash2"));
338    }
339
340    #[test]
341    fn test_session_image_context_clear_current() {
342        let mut ctx = SessionImageContext::new();
343        let metadata = create_test_image_metadata();
344
345        ctx.add_image("hash1".to_string(), metadata);
346        ctx.clear_current();
347
348        assert_eq!(ctx.current_image_count(), 0);
349        assert_eq!(ctx.total_image_count(), 1);
350    }
351
352    #[test]
353    fn test_session_image_context_clear_all() {
354        let mut ctx = SessionImageContext::new();
355        let metadata = create_test_image_metadata();
356
357        ctx.add_image("hash1".to_string(), metadata);
358        ctx.clear_all();
359
360        assert_eq!(ctx.current_image_count(), 0);
361        assert_eq!(ctx.total_image_count(), 0);
362    }
363
364    #[test]
365    fn test_session_image_context_get_metadata() {
366        let mut ctx = SessionImageContext::new();
367        let metadata = create_test_image_metadata();
368
369        ctx.add_image("hash1".to_string(), metadata.clone());
370
371        let retrieved = ctx.get_image_metadata("hash1");
372        assert!(retrieved.is_some());
373        assert_eq!(retrieved.unwrap().width, 800);
374    }
375}