Skip to main content

tauri_typegen/build/
generation_cache.rs

1use crate::interface::config::GenerateConfig;
2use crate::models::{CommandInfo, EventInfo, StructInfo};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9#[derive(Error, Debug)]
10pub enum CacheError {
11    #[error("IO error: {0}")]
12    Io(#[from] std::io::Error),
13    #[error("JSON error: {0}")]
14    Json(#[from] serde_json::Error),
15    #[error("Hash generation error: {0}")]
16    HashError(String),
17}
18
19/// Cache file name stored in the output directory
20const CACHE_FILE_NAME: &str = ".typecache";
21
22/// Represents the cached state of a generation run
23#[derive(Debug, Serialize, Deserialize)]
24pub struct GenerationCache {
25    /// Version of the cache format for future compatibility
26    version: u32,
27    /// Hash of all discovered commands
28    commands_hash: String,
29    /// Hash of all discovered structs
30    structs_hash: String,
31    /// Hash of all discovered events
32    events_hash: String,
33    /// Hash of configuration settings that affect output
34    config_hash: String,
35    /// Combined hash for quick comparison
36    combined_hash: String,
37}
38
39impl GenerationCache {
40    const CURRENT_VERSION: u32 = 2;
41
42    /// Create a new cache from current generation state
43    pub fn new(
44        commands: &[CommandInfo],
45        structs: &HashMap<String, StructInfo>,
46        events: &[EventInfo],
47        config: &GenerateConfig,
48    ) -> Result<Self, CacheError> {
49        let commands_hash = Self::hash_commands(commands)?;
50        let structs_hash = Self::hash_structs(structs)?;
51        let events_hash = Self::hash_events(events)?;
52        let config_hash = Self::hash_config(config)?;
53        let combined_hash =
54            Self::combine_hashes(&commands_hash, &structs_hash, &events_hash, &config_hash)?;
55
56        Ok(Self {
57            version: Self::CURRENT_VERSION,
58            commands_hash,
59            structs_hash,
60            events_hash,
61            config_hash,
62            combined_hash,
63        })
64    }
65
66    /// Load cache from file
67    pub fn load<P: AsRef<Path>>(output_dir: P) -> Result<Self, CacheError> {
68        let cache_path = Self::cache_path(output_dir);
69        let content = fs::read_to_string(cache_path)?;
70        let cache: Self = serde_json::from_str(&content)?;
71        Ok(cache)
72    }
73
74    /// Save cache to file
75    pub fn save<P: AsRef<Path>>(&self, output_dir: P) -> Result<(), CacheError> {
76        let cache_path = Self::cache_path(output_dir);
77
78        // Ensure output directory exists
79        if let Some(parent) = cache_path.parent() {
80            fs::create_dir_all(parent)?;
81        }
82
83        let content = serde_json::to_string_pretty(self)?;
84        fs::write(cache_path, content)?;
85        Ok(())
86    }
87
88    /// Check if generation is needed by comparing with previous cache
89    pub fn needs_regeneration<P: AsRef<Path>>(
90        output_dir: P,
91        commands: &[CommandInfo],
92        structs: &HashMap<String, StructInfo>,
93        events: &[EventInfo],
94        config: &GenerateConfig,
95    ) -> Result<bool, CacheError> {
96        // Try to load previous cache
97        let previous_cache = match Self::load(&output_dir) {
98            Ok(cache) => cache,
99            Err(_) => {
100                // No cache file or error reading it - needs regeneration
101                return Ok(true);
102            }
103        };
104
105        // Check version compatibility
106        if previous_cache.version != Self::CURRENT_VERSION {
107            return Ok(true);
108        }
109
110        // Generate current cache
111        let current_cache = Self::new(commands, structs, events, config)?;
112
113        // Compare combined hashes
114        Ok(previous_cache.combined_hash != current_cache.combined_hash)
115    }
116
117    /// Get the cache file path
118    fn cache_path<P: AsRef<Path>>(output_dir: P) -> PathBuf {
119        output_dir.as_ref().join(CACHE_FILE_NAME)
120    }
121
122    /// Generate a deterministic hash of commands
123    fn hash_commands(commands: &[CommandInfo]) -> Result<String, CacheError> {
124        // Create a serializable representation
125        #[derive(Serialize)]
126        struct CommandHashData<'a> {
127            name: &'a str,
128            serde_rename_all: Option<&'a str>,
129            parameters: Vec<ParameterHashData<'a>>,
130            return_type: &'a str,
131            is_async: bool,
132            channels: Vec<ChannelHashData<'a>>,
133        }
134
135        #[derive(Serialize)]
136        struct ParameterHashData<'a> {
137            name: &'a str,
138            rust_type: &'a str,
139            is_optional: bool,
140            serde_rename: Option<&'a str>,
141        }
142
143        #[derive(Serialize)]
144        struct ChannelHashData<'a> {
145            parameter_name: &'a str,
146            message_type: &'a str,
147            serde_rename: Option<&'a str>,
148        }
149
150        let mut serialized_commands: Vec<String> = commands
151            .iter()
152            .map(|cmd| {
153                serde_json::to_string(&CommandHashData {
154                    name: &cmd.name,
155                    serde_rename_all: cmd
156                        .serde_rename_all
157                        .as_ref()
158                        .map(|rule| rule.to_rename_all_str()),
159                    parameters: cmd
160                        .parameters
161                        .iter()
162                        .map(|p| ParameterHashData {
163                            name: &p.name,
164                            rust_type: &p.rust_type,
165                            is_optional: p.is_optional,
166                            serde_rename: p.serde_rename.as_deref(),
167                        })
168                        .collect(),
169                    return_type: &cmd.return_type,
170                    is_async: cmd.is_async,
171                    channels: cmd
172                        .channels
173                        .iter()
174                        .map(|c| ChannelHashData {
175                            parameter_name: &c.parameter_name,
176                            message_type: &c.message_type,
177                            serde_rename: c.serde_rename.as_deref(),
178                        })
179                        .collect(),
180                })
181            })
182            .collect::<Result<_, _>>()?;
183        serialized_commands.sort_unstable();
184
185        let json = serde_json::to_string(&serialized_commands)?;
186        Ok(Self::compute_hash(&json))
187    }
188
189    /// Generate a deterministic hash of events
190    fn hash_events(events: &[EventInfo]) -> Result<String, CacheError> {
191        #[derive(Serialize)]
192        struct EventHashData<'a> {
193            event_name: &'a str,
194            payload_type: &'a str,
195        }
196
197        let mut serialized_events: Vec<String> = events
198            .iter()
199            .map(|event| {
200                serde_json::to_string(&EventHashData {
201                    event_name: &event.event_name,
202                    payload_type: &event.payload_type,
203                })
204            })
205            .collect::<Result<_, _>>()?;
206        serialized_events.sort_unstable();
207
208        let json = serde_json::to_string(&serialized_events)?;
209        Ok(Self::compute_hash(&json))
210    }
211
212    /// Generate a deterministic hash of structs
213    fn hash_structs(structs: &HashMap<String, StructInfo>) -> Result<String, CacheError> {
214        #[derive(Serialize)]
215        struct StructHashData<'a> {
216            name: &'a str,
217            is_enum: bool,
218            serde_rename_all: Option<&'a str>,
219            serde_tag: Option<&'a str>,
220            fields: Vec<FieldHashData<'a>>,
221            enum_variants: Vec<EnumVariantHashData<'a>>,
222        }
223
224        #[derive(Serialize)]
225        struct FieldHashData<'a> {
226            name: &'a str,
227            rust_type: &'a str,
228            is_optional: bool,
229            is_public: bool,
230            validator_attributes: Option<&'a crate::models::ValidatorAttributes>,
231            serde_rename: Option<&'a str>,
232            type_structure: &'a crate::models::TypeStructure,
233        }
234
235        #[derive(Serialize)]
236        struct EnumVariantHashData<'a> {
237            name: &'a str,
238            serde_rename: Option<&'a str>,
239            kind: &'a crate::models::EnumVariantKind,
240        }
241
242        let mut serialized_structs: Vec<String> = structs
243            .values()
244            .map(|s| {
245                serde_json::to_string(&StructHashData {
246                    name: &s.name,
247                    is_enum: s.is_enum,
248                    serde_rename_all: s
249                        .serde_rename_all
250                        .as_ref()
251                        .map(|rule| rule.to_rename_all_str()),
252                    serde_tag: s.serde_tag.as_deref(),
253                    fields: s
254                        .fields
255                        .iter()
256                        .map(|f| FieldHashData {
257                            name: &f.name,
258                            rust_type: &f.rust_type,
259                            is_optional: f.is_optional,
260                            is_public: f.is_public,
261                            validator_attributes: f.validator_attributes.as_ref(),
262                            serde_rename: f.serde_rename.as_deref(),
263                            type_structure: &f.type_structure,
264                        })
265                        .collect(),
266                    enum_variants: s
267                        .enum_variants
268                        .as_ref()
269                        .map(|variants| {
270                            variants
271                                .iter()
272                                .map(|variant| EnumVariantHashData {
273                                    name: &variant.name,
274                                    serde_rename: variant.serde_rename.as_deref(),
275                                    kind: &variant.kind,
276                                })
277                                .collect()
278                        })
279                        .unwrap_or_default(),
280                })
281            })
282            .collect::<Result<_, _>>()?;
283        serialized_structs.sort_unstable();
284
285        let json = serde_json::to_string(&serialized_structs)?;
286        Ok(Self::compute_hash(&json))
287    }
288
289    /// Generate a hash of configuration settings that affect output
290    fn hash_config(config: &GenerateConfig) -> Result<String, CacheError> {
291        #[derive(Serialize)]
292        struct ConfigHashData<'a> {
293            validation_library: &'a str,
294            include_private: bool,
295            type_mappings: Option<Vec<(&'a str, &'a str)>>,
296            default_parameter_case: &'a str,
297            default_field_case: &'a str,
298        }
299
300        let type_mappings = config.type_mappings.as_ref().map(|mappings| {
301            let mut canonical: Vec<_> = mappings
302                .iter()
303                .map(|(key, value)| (key.as_str(), value.as_str()))
304                .collect();
305            canonical.sort_unstable();
306            canonical
307        });
308
309        let hash_data = ConfigHashData {
310            validation_library: &config.validation_library,
311            include_private: config.include_private.unwrap_or(false),
312            type_mappings,
313            default_parameter_case: &config.default_parameter_case,
314            default_field_case: &config.default_field_case,
315        };
316
317        let json = serde_json::to_string(&hash_data)?;
318        Ok(Self::compute_hash(&json))
319    }
320
321    /// Combine multiple hashes into a single hash
322    fn combine_hashes(
323        commands: &str,
324        structs: &str,
325        events: &str,
326        config: &str,
327    ) -> Result<String, CacheError> {
328        let combined = format!("{}{}{}{}", commands, structs, events, config);
329        Ok(Self::compute_hash(&combined))
330    }
331
332    /// Compute SHA-256 hash of a string
333    fn compute_hash(data: &str) -> String {
334        use std::collections::hash_map::DefaultHasher;
335        use std::hash::{Hash, Hasher};
336
337        let mut hasher = DefaultHasher::new();
338        data.hash(&mut hasher);
339        format!("{:x}", hasher.finish())
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::models::{
347        EnumVariantInfo, EnumVariantKind, FieldInfo, LengthConstraint, ParameterInfo,
348        TypeStructure, ValidatorAttributes,
349    };
350    use serde_rename_rule::RenameRule;
351    // Test utilities already imported from parent module
352    use tempfile::TempDir;
353
354    fn create_test_config() -> GenerateConfig {
355        GenerateConfig {
356            project_path: "./src-tauri".to_string(),
357            output_path: "./src/generated".to_string(),
358            validation_library: "none".to_string(),
359            verbose: Some(false),
360            visualize_deps: Some(false),
361            include_private: Some(false),
362            type_mappings: None,
363            exclude_patterns: None,
364            include_patterns: None,
365            default_parameter_case: "camelCase".to_string(),
366            default_field_case: "snake_case".to_string(),
367            force: Some(false),
368        }
369    }
370
371    fn create_test_command(name: &str) -> CommandInfo {
372        CommandInfo::new_for_test(name, "test.rs", 1, vec![], "String", false, vec![])
373    }
374
375    fn create_test_event(name: &str) -> EventInfo {
376        EventInfo {
377            event_name: name.to_string(),
378            payload_type: "String".to_string(),
379            payload_type_structure: crate::models::TypeStructure::Primitive("string".to_string()),
380            file_path: "events.rs".to_string(),
381            line_number: 1,
382        }
383    }
384
385    #[test]
386    fn test_cache_creation() {
387        let commands = vec![create_test_command("test_command")];
388        let structs = HashMap::new();
389        let config = create_test_config();
390
391        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
392
393        assert_eq!(cache.version, GenerationCache::CURRENT_VERSION);
394        assert!(!cache.commands_hash.is_empty());
395        assert!(!cache.structs_hash.is_empty());
396        assert!(!cache.config_hash.is_empty());
397        assert!(!cache.combined_hash.is_empty());
398    }
399
400    #[test]
401    fn test_cache_save_and_load() {
402        let temp_dir = TempDir::new().unwrap();
403        let commands = vec![create_test_command("test_command")];
404        let structs = HashMap::new();
405        let config = create_test_config();
406
407        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
408        cache.save(temp_dir.path()).unwrap();
409
410        let loaded_cache = GenerationCache::load(temp_dir.path()).unwrap();
411
412        assert_eq!(cache.combined_hash, loaded_cache.combined_hash);
413        assert_eq!(cache.commands_hash, loaded_cache.commands_hash);
414        assert_eq!(cache.structs_hash, loaded_cache.structs_hash);
415    }
416
417    #[test]
418    fn test_needs_regeneration_no_cache() {
419        let temp_dir = TempDir::new().unwrap();
420        let commands = vec![create_test_command("test_command")];
421        let structs = HashMap::new();
422        let config = create_test_config();
423
424        let needs_regen =
425            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &[], &config)
426                .unwrap();
427
428        assert!(needs_regen);
429    }
430
431    #[test]
432    fn test_needs_regeneration_same_state() {
433        let temp_dir = TempDir::new().unwrap();
434        let commands = vec![create_test_command("test_command")];
435        let structs = HashMap::new();
436        let config = create_test_config();
437
438        // Save initial cache
439        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
440        cache.save(temp_dir.path()).unwrap();
441
442        // Check if regeneration needed with same data
443        let needs_regen =
444            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &[], &config)
445                .unwrap();
446
447        assert!(!needs_regen);
448    }
449
450    #[test]
451    fn test_needs_regeneration_command_changed() {
452        let temp_dir = TempDir::new().unwrap();
453        let commands = vec![create_test_command("test_command")];
454        let structs = HashMap::new();
455        let config = create_test_config();
456
457        // Save initial cache
458        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
459        cache.save(temp_dir.path()).unwrap();
460
461        // Change commands
462        let new_commands = vec![create_test_command("different_command")];
463
464        let needs_regen = GenerationCache::needs_regeneration(
465            temp_dir.path(),
466            &new_commands,
467            &structs,
468            &[],
469            &config,
470        )
471        .unwrap();
472
473        assert!(needs_regen);
474    }
475
476    #[test]
477    fn test_needs_regeneration_config_changed() {
478        let temp_dir = TempDir::new().unwrap();
479        let commands = vec![create_test_command("test_command")];
480        let structs = HashMap::new();
481        let config = create_test_config();
482
483        // Save initial cache
484        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
485        cache.save(temp_dir.path()).unwrap();
486
487        // Change config
488        let mut new_config = config;
489        new_config.validation_library = "zod".to_string();
490
491        let needs_regen = GenerationCache::needs_regeneration(
492            temp_dir.path(),
493            &commands,
494            &structs,
495            &[],
496            &new_config,
497        )
498        .unwrap();
499
500        assert!(needs_regen);
501    }
502
503    #[test]
504    fn test_hash_determinism() {
505        let commands = vec![create_test_command("test_command")];
506        let structs = HashMap::new();
507        let config = create_test_config();
508
509        let cache1 = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
510        let cache2 = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
511
512        assert_eq!(cache1.combined_hash, cache2.combined_hash);
513        assert_eq!(cache1.commands_hash, cache2.commands_hash);
514        assert_eq!(cache1.structs_hash, cache2.structs_hash);
515        assert_eq!(cache1.config_hash, cache2.config_hash);
516    }
517
518    #[test]
519    fn test_needs_regeneration_version_mismatch() {
520        let temp_dir = TempDir::new().unwrap();
521        let commands = vec![create_test_command("test_command")];
522        let structs = HashMap::new();
523        let config = create_test_config();
524
525        // Create a cache with a different version
526        let old_cache_content = r#"{
527            "version": 0,
528            "commands_hash": "abc123",
529            "structs_hash": "def456",
530            "config_hash": "ghi789",
531            "combined_hash": "xyz000"
532        }"#;
533        let cache_path = temp_dir.path().join(".typecache");
534        std::fs::write(&cache_path, old_cache_content).unwrap();
535
536        // Should need regeneration due to version mismatch
537        let needs_regen =
538            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &[], &config)
539                .unwrap();
540
541        assert!(needs_regen);
542    }
543
544    #[test]
545    fn test_empty_commands_and_structs() {
546        let commands: Vec<CommandInfo> = vec![];
547        let structs: HashMap<String, crate::models::StructInfo> = HashMap::new();
548        let config = create_test_config();
549
550        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
551
552        // Should still create valid hashes even with empty data
553        assert!(!cache.commands_hash.is_empty());
554        assert!(!cache.structs_hash.is_empty());
555        assert!(!cache.combined_hash.is_empty());
556    }
557
558    #[test]
559    fn test_struct_hash_order_independence() {
560        use crate::models::{FieldInfo, StructInfo, TypeStructure};
561
562        let config = create_test_config();
563        let commands = vec![create_test_command("test_command")];
564
565        // Create two structs
566        let struct_a = StructInfo {
567            name: "StructA".to_string(),
568            fields: vec![FieldInfo {
569                name: "field_a".to_string(),
570                rust_type: "String".to_string(),
571                is_optional: false,
572                is_public: true,
573                validator_attributes: None,
574                serde_rename: None,
575                type_structure: TypeStructure::Primitive("string".to_string()),
576            }],
577            file_path: "test.rs".to_string(),
578            is_enum: false,
579            serde_rename_all: None,
580            serde_tag: None,
581            enum_variants: None,
582        };
583
584        let struct_b = StructInfo {
585            name: "StructB".to_string(),
586            fields: vec![FieldInfo {
587                name: "field_b".to_string(),
588                rust_type: "i32".to_string(),
589                is_optional: false,
590                is_public: true,
591                validator_attributes: None,
592                serde_rename: None,
593                type_structure: TypeStructure::Primitive("number".to_string()),
594            }],
595            file_path: "test.rs".to_string(),
596            is_enum: false,
597            serde_rename_all: None,
598            serde_tag: None,
599            enum_variants: None,
600        };
601
602        // Insert in order A, B
603        let mut structs1 = HashMap::new();
604        structs1.insert("StructA".to_string(), struct_a.clone());
605        structs1.insert("StructB".to_string(), struct_b.clone());
606
607        // Insert in order B, A (reverse)
608        let mut structs2 = HashMap::new();
609        structs2.insert("StructB".to_string(), struct_b);
610        structs2.insert("StructA".to_string(), struct_a);
611
612        let cache1 = GenerationCache::new(&commands, &structs1, &[], &config).unwrap();
613        let cache2 = GenerationCache::new(&commands, &structs2, &[], &config).unwrap();
614
615        // Hash should be the same regardless of insertion order
616        assert_eq!(cache1.structs_hash, cache2.structs_hash);
617        assert_eq!(cache1.combined_hash, cache2.combined_hash);
618    }
619
620    #[test]
621    fn command_hash_order_independence() {
622        let config = create_test_config();
623        let structs = HashMap::new();
624
625        let commands1 = vec![
626            create_test_command("alpha_command"),
627            create_test_command("beta_command"),
628        ];
629        let commands2 = vec![
630            create_test_command("beta_command"),
631            create_test_command("alpha_command"),
632        ];
633
634        let cache1 = GenerationCache::new(&commands1, &structs, &[], &config).unwrap();
635        let cache2 = GenerationCache::new(&commands2, &structs, &[], &config).unwrap();
636
637        assert_eq!(cache1.commands_hash, cache2.commands_hash);
638        assert_eq!(cache1.combined_hash, cache2.combined_hash);
639    }
640
641    #[test]
642    fn command_hash_ignores_source_location() {
643        let config = create_test_config();
644        let structs = HashMap::new();
645
646        let command1 = CommandInfo::new_for_test(
647            "test_command",
648            "src/alpha.rs",
649            10,
650            vec![],
651            "String",
652            false,
653            vec![],
654        );
655        let command2 = CommandInfo::new_for_test(
656            "test_command",
657            "src/beta.rs",
658            200,
659            vec![],
660            "String",
661            false,
662            vec![],
663        );
664
665        let cache1 = GenerationCache::new(&[command1], &structs, &[], &config).unwrap();
666        let cache2 = GenerationCache::new(&[command2], &structs, &[], &config).unwrap();
667
668        assert_eq!(cache1.commands_hash, cache2.commands_hash);
669        assert_eq!(cache1.combined_hash, cache2.combined_hash);
670    }
671
672    #[test]
673    fn event_hash_ignores_source_location() {
674        let config = create_test_config();
675        let commands = vec![create_test_command("test_command")];
676        let structs = HashMap::new();
677
678        let event1 = EventInfo {
679            event_name: "alpha-ready".to_string(),
680            payload_type: "String".to_string(),
681            payload_type_structure: crate::models::TypeStructure::Primitive("string".to_string()),
682            file_path: "src/alpha.rs".to_string(),
683            line_number: 10,
684        };
685        let event2 = EventInfo {
686            event_name: "alpha-ready".to_string(),
687            payload_type: "String".to_string(),
688            payload_type_structure: crate::models::TypeStructure::Primitive("string".to_string()),
689            file_path: "src/beta.rs".to_string(),
690            line_number: 200,
691        };
692
693        let cache1 = GenerationCache::new(&commands, &structs, &[event1], &config).unwrap();
694        let cache2 = GenerationCache::new(&commands, &structs, &[event2], &config).unwrap();
695
696        assert_eq!(cache1.events_hash, cache2.events_hash);
697        assert_eq!(cache1.combined_hash, cache2.combined_hash);
698    }
699
700    #[test]
701    fn struct_hash_ignores_source_location() {
702        let config = create_test_config();
703        let commands = vec![create_test_command("test_command")];
704
705        let struct1 = StructInfo {
706            name: "Payload".to_string(),
707            fields: vec![FieldInfo {
708                name: "value".to_string(),
709                rust_type: "String".to_string(),
710                is_optional: false,
711                is_public: true,
712                validator_attributes: None,
713                serde_rename: None,
714                type_structure: TypeStructure::Primitive("string".to_string()),
715            }],
716            file_path: "src/alpha.rs".to_string(),
717            is_enum: false,
718            serde_rename_all: None,
719            serde_tag: None,
720            enum_variants: None,
721        };
722        let struct2 = StructInfo {
723            file_path: "src/beta.rs".to_string(),
724            ..struct1.clone()
725        };
726
727        let mut structs1 = HashMap::new();
728        structs1.insert("Payload".to_string(), struct1);
729
730        let mut structs2 = HashMap::new();
731        structs2.insert("Payload".to_string(), struct2);
732
733        let cache1 = GenerationCache::new(&commands, &structs1, &[], &config).unwrap();
734        let cache2 = GenerationCache::new(&commands, &structs2, &[], &config).unwrap();
735
736        assert_eq!(cache1.structs_hash, cache2.structs_hash);
737        assert_eq!(cache1.combined_hash, cache2.combined_hash);
738    }
739
740    #[test]
741    fn command_hash_changes_with_serde_metadata() {
742        let config = create_test_config();
743        let structs = HashMap::new();
744
745        let mut command1 = CommandInfo::new_for_test(
746            "test_command",
747            "src/test.rs",
748            10,
749            vec![ParameterInfo {
750                name: "user_id".to_string(),
751                rust_type: "String".to_string(),
752                is_optional: false,
753                type_structure: TypeStructure::Primitive("string".to_string()),
754                serde_rename: None,
755            }],
756            "String",
757            false,
758            vec![crate::models::ChannelInfo::new_for_test(
759                "progress_updates",
760                "String",
761                "test_command",
762                "src/test.rs",
763                10,
764            )],
765        );
766        let mut command2 = CommandInfo::new_for_test(
767            "test_command",
768            "src/test.rs",
769            10,
770            vec![ParameterInfo {
771                name: "user_id".to_string(),
772                rust_type: "String".to_string(),
773                is_optional: false,
774                type_structure: TypeStructure::Primitive("string".to_string()),
775                serde_rename: Some("userIdExplicit".to_string()),
776            }],
777            "String",
778            false,
779            vec![crate::models::ChannelInfo::new_for_test(
780                "progress_updates",
781                "String",
782                "test_command",
783                "src/test.rs",
784                10,
785            )],
786        );
787        command1.serde_rename_all = Some(RenameRule::SnakeCase);
788        command2.channels[0].serde_rename = Some("progressUpdates".to_string());
789
790        let cache1 = GenerationCache::new(&[command1], &structs, &[], &config).unwrap();
791        let cache2 = GenerationCache::new(&[command2], &structs, &[], &config).unwrap();
792
793        assert_ne!(cache1.commands_hash, cache2.commands_hash);
794        assert_ne!(cache1.combined_hash, cache2.combined_hash);
795    }
796
797    #[test]
798    fn struct_hash_changes_with_field_metadata() {
799        let config = create_test_config();
800        let commands = vec![create_test_command("test_command")];
801
802        let struct1 = StructInfo {
803            name: "Payload".to_string(),
804            fields: vec![FieldInfo {
805                name: "created_at".to_string(),
806                rust_type: "String".to_string(),
807                is_optional: false,
808                is_public: true,
809                validator_attributes: None,
810                serde_rename: None,
811                type_structure: TypeStructure::Primitive("string".to_string()),
812            }],
813            file_path: "src/payload.rs".to_string(),
814            is_enum: false,
815            serde_rename_all: None,
816            serde_tag: None,
817            enum_variants: None,
818        };
819        let struct2 = StructInfo {
820            fields: vec![FieldInfo {
821                name: "created_at".to_string(),
822                rust_type: "String".to_string(),
823                is_optional: false,
824                is_public: true,
825                validator_attributes: Some(ValidatorAttributes {
826                    length: Some(LengthConstraint {
827                        min: Some(1),
828                        max: None,
829                        message: Some("required".to_string()),
830                    }),
831                    range: None,
832                    email: false,
833                    url: false,
834                    custom_message: Some("required".to_string()),
835                }),
836                serde_rename: Some("createdAt".to_string()),
837                type_structure: TypeStructure::Primitive("string".to_string()),
838            }],
839            serde_rename_all: Some(RenameRule::CamelCase),
840            ..struct1.clone()
841        };
842
843        let mut structs1 = HashMap::new();
844        structs1.insert("Payload".to_string(), struct1);
845
846        let mut structs2 = HashMap::new();
847        structs2.insert("Payload".to_string(), struct2);
848
849        let cache1 = GenerationCache::new(&commands, &structs1, &[], &config).unwrap();
850        let cache2 = GenerationCache::new(&commands, &structs2, &[], &config).unwrap();
851
852        assert_ne!(cache1.structs_hash, cache2.structs_hash);
853        assert_ne!(cache1.combined_hash, cache2.combined_hash);
854    }
855
856    #[test]
857    fn struct_hash_changes_with_enum_metadata() {
858        let config = create_test_config();
859        let commands = vec![create_test_command("test_command")];
860
861        let base_variant = EnumVariantInfo {
862            name: "ReadyState".to_string(),
863            kind: EnumVariantKind::Struct(vec![FieldInfo {
864                name: "event_id".to_string(),
865                rust_type: "String".to_string(),
866                is_optional: false,
867                is_public: true,
868                validator_attributes: None,
869                serde_rename: None,
870                type_structure: TypeStructure::Primitive("string".to_string()),
871            }]),
872            serde_rename: None,
873        };
874        let renamed_variant = EnumVariantInfo {
875            serde_rename: Some("ready_state".to_string()),
876            ..base_variant.clone()
877        };
878
879        let enum1 = StructInfo {
880            name: "StatusEvent".to_string(),
881            fields: vec![],
882            file_path: "src/status.rs".to_string(),
883            is_enum: true,
884            serde_rename_all: None,
885            serde_tag: None,
886            enum_variants: Some(vec![base_variant]),
887        };
888        let enum2 = StructInfo {
889            serde_rename_all: Some(RenameRule::SnakeCase),
890            serde_tag: Some("kind".to_string()),
891            enum_variants: Some(vec![renamed_variant]),
892            ..enum1.clone()
893        };
894
895        let mut structs1 = HashMap::new();
896        structs1.insert("StatusEvent".to_string(), enum1);
897
898        let mut structs2 = HashMap::new();
899        structs2.insert("StatusEvent".to_string(), enum2);
900
901        let cache1 = GenerationCache::new(&commands, &structs1, &[], &config).unwrap();
902        let cache2 = GenerationCache::new(&commands, &structs2, &[], &config).unwrap();
903
904        assert_ne!(cache1.structs_hash, cache2.structs_hash);
905        assert_ne!(cache1.combined_hash, cache2.combined_hash);
906    }
907
908    #[test]
909    fn test_needs_regeneration_with_corrupted_cache_file() {
910        let temp_dir = TempDir::new().unwrap();
911        let commands = vec![create_test_command("test_command")];
912        let structs = HashMap::new();
913        let config = create_test_config();
914
915        // Create a corrupted cache file
916        let cache_path = temp_dir.path().join(".typecache");
917        std::fs::write(&cache_path, "not valid json").unwrap();
918
919        // Should need regeneration because cache is unreadable
920        let needs_regen =
921            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &[], &config)
922                .unwrap();
923
924        assert!(needs_regen);
925    }
926
927    #[test]
928    fn test_cache_with_type_mappings_config() {
929        let commands = vec![create_test_command("test_command")];
930        let structs = HashMap::new();
931
932        let mut config1 = create_test_config();
933        let mut type_mappings = std::collections::HashMap::new();
934        type_mappings.insert("CustomType".to_string(), "string".to_string());
935        config1.type_mappings = Some(type_mappings);
936
937        let config2 = create_test_config(); // No type mappings
938
939        let cache1 = GenerationCache::new(&commands, &structs, &[], &config1).unwrap();
940        let cache2 = GenerationCache::new(&commands, &structs, &[], &config2).unwrap();
941
942        // Config hash should differ when type_mappings differ
943        assert_ne!(cache1.config_hash, cache2.config_hash);
944        assert_ne!(cache1.combined_hash, cache2.combined_hash);
945    }
946
947    #[test]
948    fn config_hash_type_mappings_order_independence() {
949        let commands = vec![create_test_command("test_command")];
950        let structs = HashMap::new();
951
952        let mut config1 = create_test_config();
953        let mut mappings1 = HashMap::new();
954        mappings1.insert("First".to_string(), "string".to_string());
955        mappings1.insert("Second".to_string(), "number".to_string());
956        config1.type_mappings = Some(mappings1);
957
958        let mut config2 = create_test_config();
959        let mut mappings2 = HashMap::new();
960        mappings2.insert("Second".to_string(), "number".to_string());
961        mappings2.insert("First".to_string(), "string".to_string());
962        config2.type_mappings = Some(mappings2);
963
964        let cache1 = GenerationCache::new(&commands, &structs, &[], &config1).unwrap();
965        let cache2 = GenerationCache::new(&commands, &structs, &[], &config2).unwrap();
966
967        assert_eq!(cache1.config_hash, cache2.config_hash);
968        assert_eq!(cache1.combined_hash, cache2.combined_hash);
969    }
970
971    #[test]
972    fn events_change_requires_regeneration() {
973        let temp_dir = TempDir::new().unwrap();
974        let commands = vec![create_test_command("test_command")];
975        let structs = HashMap::new();
976        let config = create_test_config();
977        let initial_events = vec![create_test_event("alpha-ready")];
978        let changed_events = vec![create_test_event("beta-ready")];
979
980        let cache = GenerationCache::new(&commands, &structs, &initial_events, &config).unwrap();
981        cache.save(temp_dir.path()).unwrap();
982
983        let needs_regen = GenerationCache::needs_regeneration(
984            temp_dir.path(),
985            &commands,
986            &structs,
987            &changed_events,
988            &config,
989        )
990        .unwrap();
991
992        assert!(needs_regen);
993    }
994
995    #[test]
996    fn test_cache_with_channels() {
997        use crate::models::ChannelInfo;
998
999        let structs = HashMap::new();
1000        let config = create_test_config();
1001
1002        let channel = ChannelInfo::new_for_test("progress", "u32", "test_command", "test.rs", 1);
1003
1004        let cmd_with_channel = CommandInfo::new_for_test(
1005            "test_command",
1006            "test.rs",
1007            1,
1008            vec![],
1009            "String",
1010            false,
1011            vec![channel],
1012        );
1013
1014        let cmd_without_channel = create_test_command("test_command");
1015
1016        let cache_with = GenerationCache::new(&[cmd_with_channel], &structs, &[], &config).unwrap();
1017        let cache_without =
1018            GenerationCache::new(&[cmd_without_channel], &structs, &[], &config).unwrap();
1019
1020        // Commands hash should differ when channels differ
1021        assert_ne!(cache_with.commands_hash, cache_without.commands_hash);
1022    }
1023
1024    #[test]
1025    fn test_save_creates_output_directory() {
1026        let temp_dir = TempDir::new().unwrap();
1027        let nested_output = temp_dir.path().join("nested").join("output").join("dir");
1028
1029        let commands = vec![create_test_command("test_command")];
1030        let structs = HashMap::new();
1031        let config = create_test_config();
1032
1033        let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
1034
1035        // Should create nested directories
1036        cache.save(&nested_output).unwrap();
1037
1038        assert!(nested_output.join(".typecache").exists());
1039    }
1040
1041    #[test]
1042    fn test_load_nonexistent_cache() {
1043        let temp_dir = TempDir::new().unwrap();
1044
1045        // Should return an error when cache doesn't exist
1046        let result = GenerationCache::load(temp_dir.path());
1047        assert!(result.is_err());
1048    }
1049}