ragfs_core/
types.rs

1//! Core types for RAGFS.
2//!
3//! This module contains all shared data structures used across RAGFS:
4//!
5//! ## File Management
6//! - [`FileRecord`]: Metadata about an indexed file
7//! - [`FileStatus`]: Current indexing state of a file
8//! - [`FileEvent`]: File system events for the watcher
9//!
10//! ## Content Chunks
11//! - [`Chunk`]: A segment of content with its embedding
12//! - [`ContentType`]: Type classification for chunk content
13//! - [`ChunkConfig`]: Configuration for chunking behavior
14//!
15//! ## Extraction
16//! - [`ExtractedContent`]: Content extracted from a file
17//! - [`ContentElement`]: Structural elements (headings, paragraphs, etc.)
18//!
19//! ## Embeddings
20//! - [`Modality`]: Supported embedding modalities (text, image, audio)
21//! - [`EmbeddingConfig`]: Configuration for embedding generation
22//! - [`EmbeddingOutput`]: Result of embedding a text
23//!
24//! ## Search
25//! - [`SearchQuery`]: Parameters for a vector search
26//! - [`SearchResult`]: A matching chunk with similarity score
27//! - [`SearchFilter`]: Filters to narrow search results
28//! - [`DistanceMetric`]: Vector distance calculation method
29
30use chrono::{DateTime, Utc};
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::ops::Range;
34use std::path::PathBuf;
35use uuid::Uuid;
36
37// ============================================================================
38// File Records
39// ============================================================================
40
41/// Metadata about an indexed file.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct FileRecord {
44    /// Unique file identifier
45    pub id: Uuid,
46    /// Absolute path to the file
47    pub path: PathBuf,
48    /// File size in bytes
49    pub size_bytes: u64,
50    /// MIME type
51    pub mime_type: String,
52    /// Content hash for change detection (blake3)
53    pub content_hash: String,
54    /// Last modification time
55    pub modified_at: DateTime<Utc>,
56    /// When the file was indexed (None if not yet indexed)
57    pub indexed_at: Option<DateTime<Utc>>,
58    /// Number of chunks produced
59    pub chunk_count: u32,
60    /// Current indexing status
61    pub status: FileStatus,
62    /// Error message if status is Error
63    pub error_message: Option<String>,
64}
65
66/// File indexing status.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "lowercase")]
69pub enum FileStatus {
70    /// Waiting to be indexed
71    Pending,
72    /// Currently being indexed
73    Indexing,
74    /// Successfully indexed
75    Indexed,
76    /// Indexing failed
77    Error,
78    /// File was deleted
79    Deleted,
80}
81
82// ============================================================================
83// Chunks
84// ============================================================================
85
86/// A chunk of content from a file.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Chunk {
89    /// Unique chunk identifier
90    pub id: Uuid,
91    /// Parent file identifier
92    pub file_id: Uuid,
93    /// Path to the source file
94    pub file_path: PathBuf,
95    /// The actual content
96    pub content: String,
97    /// Type of content
98    pub content_type: ContentType,
99    /// MIME type of the source file
100    pub mime_type: Option<String>,
101    /// Position in file (0-indexed)
102    pub chunk_index: u32,
103    /// Byte range in source file
104    pub byte_range: Range<u64>,
105    /// Line range (if applicable)
106    pub line_range: Option<Range<u32>>,
107    /// Parent chunk ID (for hierarchical chunking)
108    pub parent_chunk_id: Option<Uuid>,
109    /// Depth in hierarchy (0 = root)
110    pub depth: u8,
111    /// Embedding vector (if computed)
112    pub embedding: Option<Vec<f32>>,
113    /// Additional metadata
114    pub metadata: ChunkMetadata,
115}
116
117/// Type of chunk content.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum ContentType {
121    /// Plain text content
122    Text,
123    /// Source code
124    Code {
125        /// Programming language
126        language: String,
127        /// Code symbol information
128        symbol: Option<CodeSymbol>,
129    },
130    /// Caption for an image
131    ImageCaption,
132    /// Content from a PDF page
133    PdfPage {
134        /// Page number (1-indexed)
135        page_num: u32,
136    },
137    /// Markdown content
138    Markdown,
139}
140
141/// Code symbol information.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct CodeSymbol {
144    /// Type of symbol
145    pub kind: SymbolKind,
146    /// Symbol name
147    pub name: String,
148}
149
150/// Types of code symbols.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum SymbolKind {
154    Function,
155    Method,
156    Class,
157    Struct,
158    Enum,
159    Module,
160    Constant,
161    Variable,
162    Interface,
163    Trait,
164}
165
166/// Metadata associated with a chunk.
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct ChunkMetadata {
169    /// Embedding model used
170    pub embedding_model: Option<String>,
171    /// When chunk was indexed
172    pub indexed_at: Option<DateTime<Utc>>,
173    /// Token count (approximate)
174    pub token_count: Option<usize>,
175    /// Additional key-value metadata
176    #[serde(flatten)]
177    pub extra: HashMap<String, String>,
178}
179
180// ============================================================================
181// Extraction
182// ============================================================================
183
184/// Content extracted from a file.
185#[derive(Debug, Clone)]
186pub struct ExtractedContent {
187    /// Main text content
188    pub text: String,
189    /// Structured elements
190    pub elements: Vec<ContentElement>,
191    /// Extracted images
192    pub images: Vec<ExtractedImage>,
193    /// File-level metadata
194    pub metadata: ContentMetadataInfo,
195}
196
197/// A structural element in extracted content.
198#[derive(Debug, Clone)]
199pub enum ContentElement {
200    Heading {
201        level: u8,
202        text: String,
203        byte_offset: u64,
204    },
205    Paragraph {
206        text: String,
207        byte_offset: u64,
208    },
209    CodeBlock {
210        language: Option<String>,
211        code: String,
212        byte_offset: u64,
213    },
214    List {
215        items: Vec<String>,
216        ordered: bool,
217        byte_offset: u64,
218    },
219    Table {
220        headers: Vec<String>,
221        rows: Vec<Vec<String>>,
222        byte_offset: u64,
223    },
224}
225
226/// An image extracted from a document.
227#[derive(Debug, Clone)]
228pub struct ExtractedImage {
229    /// Raw image data
230    pub data: Vec<u8>,
231    /// MIME type
232    pub mime_type: String,
233    /// Caption if available
234    pub caption: Option<String>,
235    /// Page number (for PDFs)
236    pub page: Option<u32>,
237}
238
239/// Metadata extracted from file content.
240#[derive(Debug, Clone, Default)]
241pub struct ContentMetadataInfo {
242    /// Document title
243    pub title: Option<String>,
244    /// Author
245    pub author: Option<String>,
246    /// Language
247    pub language: Option<String>,
248    /// Page count (for PDFs)
249    pub page_count: Option<u32>,
250    /// Creation date
251    pub created_at: Option<DateTime<Utc>>,
252}
253
254// ============================================================================
255// Chunking
256// ============================================================================
257
258/// Configuration for chunking.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct ChunkConfig {
261    /// Target chunk size in tokens
262    pub target_size: usize,
263    /// Maximum chunk size in tokens
264    pub max_size: usize,
265    /// Overlap between chunks in tokens
266    pub overlap: usize,
267    /// Enable hierarchical chunking
268    pub hierarchical: bool,
269    /// Maximum hierarchy depth
270    pub max_depth: u8,
271}
272
273impl Default for ChunkConfig {
274    fn default() -> Self {
275        Self {
276            target_size: 512,
277            max_size: 1024,
278            overlap: 64,
279            hierarchical: true,
280            max_depth: 2,
281        }
282    }
283}
284
285/// Output from a chunker.
286#[derive(Debug, Clone)]
287pub struct ChunkOutput {
288    /// Chunk content
289    pub content: String,
290    /// Byte range in source
291    pub byte_range: Range<u64>,
292    /// Line range if applicable
293    pub line_range: Option<Range<u32>>,
294    /// Index of parent chunk (in output array)
295    pub parent_index: Option<usize>,
296    /// Depth in hierarchy
297    pub depth: u8,
298    /// Additional metadata
299    pub metadata: ChunkOutputMetadata,
300}
301
302/// Metadata for chunk output.
303#[derive(Debug, Clone, Default)]
304pub struct ChunkOutputMetadata {
305    /// Symbol type (for code)
306    pub symbol_type: Option<String>,
307    /// Symbol name (for code)
308    pub symbol_name: Option<String>,
309    /// Programming language
310    pub language: Option<String>,
311}
312
313// ============================================================================
314// Embedding
315// ============================================================================
316
317/// Supported modalities for embedding.
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(rename_all = "lowercase")]
320pub enum Modality {
321    Text,
322    Image,
323    Audio,
324}
325
326/// Configuration for embedding.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct EmbeddingConfig {
329    /// Normalize embeddings to unit length
330    pub normalize: bool,
331    /// Instruction prefix for models that support it
332    pub instruction: Option<String>,
333    /// Batch size for processing
334    pub batch_size: usize,
335}
336
337impl Default for EmbeddingConfig {
338    fn default() -> Self {
339        Self {
340            normalize: true,
341            instruction: None,
342            batch_size: 32,
343        }
344    }
345}
346
347/// Output from embedding.
348#[derive(Debug, Clone)]
349pub struct EmbeddingOutput {
350    /// The embedding vector
351    pub embedding: Vec<f32>,
352    /// Number of tokens in input
353    pub token_count: usize,
354}
355
356// ============================================================================
357// Search
358// ============================================================================
359
360/// A search query.
361#[derive(Debug, Clone)]
362pub struct SearchQuery {
363    /// Query embedding
364    pub embedding: Vec<f32>,
365    /// Optional text for hybrid search
366    pub text: Option<String>,
367    /// Maximum results to return
368    pub limit: usize,
369    /// Search filters
370    pub filters: Vec<SearchFilter>,
371    /// Distance metric
372    pub metric: DistanceMetric,
373}
374
375/// Search filters.
376#[derive(Debug, Clone)]
377pub enum SearchFilter {
378    /// Match files with path prefix
379    PathPrefix(String),
380    /// Match files by glob pattern
381    PathGlob(String),
382    /// Match by MIME type
383    MimeType(String),
384    /// Match by programming language
385    Language(String),
386    /// Files modified after date
387    ModifiedAfter(DateTime<Utc>),
388    /// Files modified before date
389    ModifiedBefore(DateTime<Utc>),
390    /// Minimum hierarchy depth
391    MinDepth(u8),
392    /// Maximum hierarchy depth
393    MaxDepth(u8),
394}
395
396/// Distance metric for vector search.
397#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(rename_all = "lowercase")]
399pub enum DistanceMetric {
400    #[default]
401    Cosine,
402    L2,
403    Dot,
404}
405
406/// A search result.
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct SearchResult {
409    /// Chunk ID
410    pub chunk_id: Uuid,
411    /// File path
412    pub file_path: PathBuf,
413    /// Chunk content
414    pub content: String,
415    /// Similarity score
416    pub score: f32,
417    /// Byte range in file
418    pub byte_range: Range<u64>,
419    /// Line range if available
420    pub line_range: Option<Range<u32>>,
421    /// Additional metadata
422    pub metadata: HashMap<String, String>,
423}
424
425/// Vector store statistics.
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct StoreStats {
428    /// Total number of chunks
429    pub total_chunks: u64,
430    /// Total number of files
431    pub total_files: u64,
432    /// Index size in bytes
433    pub index_size_bytes: u64,
434    /// Last update time
435    pub last_updated: Option<DateTime<Utc>>,
436}
437
438// ============================================================================
439// Index Status
440// ============================================================================
441
442/// Overall index statistics.
443#[derive(Debug, Clone, Default, Serialize, Deserialize)]
444pub struct IndexStats {
445    /// Total files tracked
446    pub total_files: u64,
447    /// Successfully indexed files
448    pub indexed_files: u64,
449    /// Files pending indexing
450    pub pending_files: u64,
451    /// Files with errors
452    pub error_files: u64,
453    /// Total chunks stored
454    pub total_chunks: u64,
455    /// Last update time
456    pub last_update: Option<DateTime<Utc>>,
457}
458
459// ============================================================================
460// File Events
461// ============================================================================
462
463/// File system event for indexing.
464#[derive(Debug, Clone)]
465pub enum FileEvent {
466    Created(PathBuf),
467    Modified(PathBuf),
468    Deleted(PathBuf),
469    Renamed { from: PathBuf, to: PathBuf },
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    // ==================== FileRecord Tests ====================
477
478    #[test]
479    fn test_file_record_serialization() {
480        let record = FileRecord {
481            id: Uuid::new_v4(),
482            path: PathBuf::from("/test/file.txt"),
483            size_bytes: 1024,
484            mime_type: "text/plain".to_string(),
485            content_hash: "abc123".to_string(),
486            modified_at: Utc::now(),
487            indexed_at: Some(Utc::now()),
488            chunk_count: 5,
489            status: FileStatus::Indexed,
490            error_message: None,
491        };
492
493        let json = serde_json::to_string(&record).unwrap();
494        let deserialized: FileRecord = serde_json::from_str(&json).unwrap();
495
496        assert_eq!(record.id, deserialized.id);
497        assert_eq!(record.path, deserialized.path);
498        assert_eq!(record.size_bytes, deserialized.size_bytes);
499        assert_eq!(record.status, deserialized.status);
500    }
501
502    #[test]
503    fn test_file_status_serialization() {
504        assert_eq!(
505            serde_json::to_string(&FileStatus::Pending).unwrap(),
506            "\"pending\""
507        );
508        assert_eq!(
509            serde_json::to_string(&FileStatus::Indexed).unwrap(),
510            "\"indexed\""
511        );
512        assert_eq!(
513            serde_json::to_string(&FileStatus::Error).unwrap(),
514            "\"error\""
515        );
516    }
517
518    #[test]
519    fn test_file_status_equality() {
520        assert_eq!(FileStatus::Pending, FileStatus::Pending);
521        assert_ne!(FileStatus::Pending, FileStatus::Indexed);
522    }
523
524    // ==================== Chunk Tests ====================
525
526    #[test]
527    fn test_chunk_serialization() {
528        let chunk = Chunk {
529            id: Uuid::new_v4(),
530            file_id: Uuid::new_v4(),
531            file_path: PathBuf::from("/test/file.rs"),
532            content: "fn main() {}".to_string(),
533            content_type: ContentType::Code {
534                language: "rust".to_string(),
535                symbol: Some(CodeSymbol {
536                    kind: SymbolKind::Function,
537                    name: "main".to_string(),
538                }),
539            },
540            mime_type: Some("text/x-rust".to_string()),
541            chunk_index: 0,
542            byte_range: 0..12,
543            line_range: Some(0..1),
544            parent_chunk_id: None,
545            depth: 0,
546            embedding: None,
547            metadata: ChunkMetadata::default(),
548        };
549
550        let json = serde_json::to_string(&chunk).unwrap();
551        let deserialized: Chunk = serde_json::from_str(&json).unwrap();
552
553        assert_eq!(chunk.id, deserialized.id);
554        assert_eq!(chunk.content, deserialized.content);
555    }
556
557    #[test]
558    fn test_content_type_text() {
559        let ct = ContentType::Text;
560        let json = serde_json::to_string(&ct).unwrap();
561        assert!(json.contains("\"type\":\"text\""));
562    }
563
564    #[test]
565    fn test_content_type_code() {
566        let ct = ContentType::Code {
567            language: "python".to_string(),
568            symbol: None,
569        };
570        let json = serde_json::to_string(&ct).unwrap();
571        assert!(json.contains("\"type\":\"code\""));
572        assert!(json.contains("\"language\":\"python\""));
573    }
574
575    #[test]
576    fn test_content_type_pdf_page() {
577        let ct = ContentType::PdfPage { page_num: 5 };
578        let json = serde_json::to_string(&ct).unwrap();
579        assert!(json.contains("\"type\":\"pdf_page\""));
580        assert!(json.contains("\"page_num\":5"));
581    }
582
583    #[test]
584    fn test_content_type_markdown() {
585        let ct = ContentType::Markdown;
586        let json = serde_json::to_string(&ct).unwrap();
587        assert!(json.contains("\"type\":\"markdown\""));
588    }
589
590    #[test]
591    fn test_symbol_kind_serialization() {
592        assert_eq!(
593            serde_json::to_string(&SymbolKind::Function).unwrap(),
594            "\"function\""
595        );
596        assert_eq!(
597            serde_json::to_string(&SymbolKind::Struct).unwrap(),
598            "\"struct\""
599        );
600        assert_eq!(
601            serde_json::to_string(&SymbolKind::Trait).unwrap(),
602            "\"trait\""
603        );
604    }
605
606    // ==================== ChunkConfig Tests ====================
607
608    #[test]
609    fn test_chunk_config_default() {
610        let config = ChunkConfig::default();
611        assert_eq!(config.target_size, 512);
612        assert_eq!(config.max_size, 1024);
613        assert_eq!(config.overlap, 64);
614        assert!(config.hierarchical);
615        assert_eq!(config.max_depth, 2);
616    }
617
618    #[test]
619    fn test_chunk_config_serialization() {
620        let config = ChunkConfig::default();
621        let json = serde_json::to_string(&config).unwrap();
622        let deserialized: ChunkConfig = serde_json::from_str(&json).unwrap();
623
624        assert_eq!(config.target_size, deserialized.target_size);
625        assert_eq!(config.max_size, deserialized.max_size);
626    }
627
628    // ==================== EmbeddingConfig Tests ====================
629
630    #[test]
631    fn test_embedding_config_default() {
632        let config = EmbeddingConfig::default();
633        assert!(config.normalize);
634        assert!(config.instruction.is_none());
635        assert_eq!(config.batch_size, 32);
636    }
637
638    #[test]
639    fn test_embedding_config_serialization() {
640        let config = EmbeddingConfig {
641            normalize: false,
642            instruction: Some("Search: ".to_string()),
643            batch_size: 16,
644        };
645        let json = serde_json::to_string(&config).unwrap();
646        let deserialized: EmbeddingConfig = serde_json::from_str(&json).unwrap();
647
648        assert_eq!(config.normalize, deserialized.normalize);
649        assert_eq!(config.instruction, deserialized.instruction);
650        assert_eq!(config.batch_size, deserialized.batch_size);
651    }
652
653    // ==================== Modality Tests ====================
654
655    #[test]
656    fn test_modality_serialization() {
657        assert_eq!(serde_json::to_string(&Modality::Text).unwrap(), "\"text\"");
658        assert_eq!(
659            serde_json::to_string(&Modality::Image).unwrap(),
660            "\"image\""
661        );
662        assert_eq!(
663            serde_json::to_string(&Modality::Audio).unwrap(),
664            "\"audio\""
665        );
666    }
667
668    #[test]
669    fn test_modality_equality() {
670        assert_eq!(Modality::Text, Modality::Text);
671        assert_ne!(Modality::Text, Modality::Image);
672    }
673
674    // ==================== DistanceMetric Tests ====================
675
676    #[test]
677    fn test_distance_metric_default() {
678        let metric = DistanceMetric::default();
679        assert_eq!(metric, DistanceMetric::Cosine);
680    }
681
682    #[test]
683    fn test_distance_metric_serialization() {
684        assert_eq!(
685            serde_json::to_string(&DistanceMetric::Cosine).unwrap(),
686            "\"cosine\""
687        );
688        assert_eq!(
689            serde_json::to_string(&DistanceMetric::L2).unwrap(),
690            "\"l2\""
691        );
692        assert_eq!(
693            serde_json::to_string(&DistanceMetric::Dot).unwrap(),
694            "\"dot\""
695        );
696    }
697
698    // ==================== SearchResult Tests ====================
699
700    #[test]
701    fn test_search_result_serialization() {
702        let result = SearchResult {
703            chunk_id: Uuid::new_v4(),
704            file_path: PathBuf::from("/test/file.txt"),
705            content: "Test content".to_string(),
706            score: 0.95,
707            byte_range: 0..12,
708            line_range: Some(0..1),
709            metadata: HashMap::new(),
710        };
711
712        let json = serde_json::to_string(&result).unwrap();
713        let deserialized: SearchResult = serde_json::from_str(&json).unwrap();
714
715        assert_eq!(result.chunk_id, deserialized.chunk_id);
716        assert_eq!(result.score, deserialized.score);
717        assert_eq!(result.content, deserialized.content);
718    }
719
720    // ==================== StoreStats Tests ====================
721
722    #[test]
723    fn test_store_stats_serialization() {
724        let stats = StoreStats {
725            total_chunks: 100,
726            total_files: 10,
727            index_size_bytes: 1024 * 1024,
728            last_updated: Some(Utc::now()),
729        };
730
731        let json = serde_json::to_string(&stats).unwrap();
732        let deserialized: StoreStats = serde_json::from_str(&json).unwrap();
733
734        assert_eq!(stats.total_chunks, deserialized.total_chunks);
735        assert_eq!(stats.total_files, deserialized.total_files);
736    }
737
738    // ==================== IndexStats Tests ====================
739
740    #[test]
741    fn test_index_stats_default() {
742        let stats = IndexStats::default();
743        assert_eq!(stats.total_files, 0);
744        assert_eq!(stats.indexed_files, 0);
745        assert_eq!(stats.pending_files, 0);
746        assert_eq!(stats.error_files, 0);
747        assert_eq!(stats.total_chunks, 0);
748        assert!(stats.last_update.is_none());
749    }
750
751    #[test]
752    fn test_index_stats_serialization() {
753        let stats = IndexStats {
754            total_files: 50,
755            indexed_files: 45,
756            pending_files: 3,
757            error_files: 2,
758            total_chunks: 500,
759            last_update: Some(Utc::now()),
760        };
761
762        let json = serde_json::to_string(&stats).unwrap();
763        let deserialized: IndexStats = serde_json::from_str(&json).unwrap();
764
765        assert_eq!(stats.total_files, deserialized.total_files);
766        assert_eq!(stats.indexed_files, deserialized.indexed_files);
767    }
768
769    // ==================== ChunkMetadata Tests ====================
770
771    #[test]
772    fn test_chunk_metadata_default() {
773        let meta = ChunkMetadata::default();
774        assert!(meta.embedding_model.is_none());
775        assert!(meta.indexed_at.is_none());
776        assert!(meta.token_count.is_none());
777        assert!(meta.extra.is_empty());
778    }
779
780    // ==================== ChunkOutput Tests ====================
781
782    #[test]
783    fn test_chunk_output_metadata_default() {
784        let meta = ChunkOutputMetadata::default();
785        assert!(meta.symbol_type.is_none());
786        assert!(meta.symbol_name.is_none());
787        assert!(meta.language.is_none());
788    }
789
790    // ==================== ContentMetadataInfo Tests ====================
791
792    #[test]
793    fn test_content_metadata_info_default() {
794        let meta = ContentMetadataInfo::default();
795        assert!(meta.title.is_none());
796        assert!(meta.author.is_none());
797        assert!(meta.language.is_none());
798        assert!(meta.page_count.is_none());
799        assert!(meta.created_at.is_none());
800    }
801
802    // ==================== FileEvent Tests ====================
803
804    #[test]
805    fn test_file_event_created() {
806        let event = FileEvent::Created(PathBuf::from("/test/new.txt"));
807        match event {
808            FileEvent::Created(path) => assert_eq!(path, PathBuf::from("/test/new.txt")),
809            _ => panic!("Expected Created event"),
810        }
811    }
812
813    #[test]
814    fn test_file_event_modified() {
815        let event = FileEvent::Modified(PathBuf::from("/test/changed.txt"));
816        match event {
817            FileEvent::Modified(path) => assert_eq!(path, PathBuf::from("/test/changed.txt")),
818            _ => panic!("Expected Modified event"),
819        }
820    }
821
822    #[test]
823    fn test_file_event_deleted() {
824        let event = FileEvent::Deleted(PathBuf::from("/test/removed.txt"));
825        match event {
826            FileEvent::Deleted(path) => assert_eq!(path, PathBuf::from("/test/removed.txt")),
827            _ => panic!("Expected Deleted event"),
828        }
829    }
830
831    #[test]
832    fn test_file_event_renamed() {
833        let event = FileEvent::Renamed {
834            from: PathBuf::from("/test/old.txt"),
835            to: PathBuf::from("/test/new.txt"),
836        };
837        match event {
838            FileEvent::Renamed { from, to } => {
839                assert_eq!(from, PathBuf::from("/test/old.txt"));
840                assert_eq!(to, PathBuf::from("/test/new.txt"));
841            }
842            _ => panic!("Expected Renamed event"),
843        }
844    }
845}