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
19const CACHE_FILE_NAME: &str = ".typecache";
21
22#[derive(Debug, Serialize, Deserialize)]
24pub struct GenerationCache {
25 version: u32,
27 commands_hash: String,
29 structs_hash: String,
31 events_hash: String,
33 config_hash: String,
35 combined_hash: String,
37}
38
39impl GenerationCache {
40 const CURRENT_VERSION: u32 = 2;
41
42 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 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 pub fn save<P: AsRef<Path>>(&self, output_dir: P) -> Result<(), CacheError> {
76 let cache_path = Self::cache_path(output_dir);
77
78 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 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 let previous_cache = match Self::load(&output_dir) {
98 Ok(cache) => cache,
99 Err(_) => {
100 return Ok(true);
102 }
103 };
104
105 if previous_cache.version != Self::CURRENT_VERSION {
107 return Ok(true);
108 }
109
110 let current_cache = Self::new(commands, structs, events, config)?;
112
113 Ok(previous_cache.combined_hash != current_cache.combined_hash)
115 }
116
117 fn cache_path<P: AsRef<Path>>(output_dir: P) -> PathBuf {
119 output_dir.as_ref().join(CACHE_FILE_NAME)
120 }
121
122 fn hash_commands(commands: &[CommandInfo]) -> Result<String, CacheError> {
124 #[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 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 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 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 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 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 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 let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
440 cache.save(temp_dir.path()).unwrap();
441
442 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 let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
459 cache.save(temp_dir.path()).unwrap();
460
461 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 let cache = GenerationCache::new(&commands, &structs, &[], &config).unwrap();
485 cache.save(temp_dir.path()).unwrap();
486
487 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 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 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 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 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 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 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 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 let cache_path = temp_dir.path().join(".typecache");
917 std::fs::write(&cache_path, "not valid json").unwrap();
918
919 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(); let cache1 = GenerationCache::new(&commands, &structs, &[], &config1).unwrap();
940 let cache2 = GenerationCache::new(&commands, &structs, &[], &config2).unwrap();
941
942 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 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 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 let result = GenerationCache::load(temp_dir.path());
1047 assert!(result.is_err());
1048 }
1049}