Skip to main content

tauri_typegen/build/
generation_cache.rs

1use crate::interface::config::GenerateConfig;
2use crate::models::{CommandInfo, 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 configuration settings that affect output
32    config_hash: String,
33    /// Combined hash for quick comparison
34    combined_hash: String,
35}
36
37impl GenerationCache {
38    const CURRENT_VERSION: u32 = 1;
39
40    /// Create a new cache from current generation state
41    pub fn new(
42        commands: &[CommandInfo],
43        structs: &HashMap<String, StructInfo>,
44        config: &GenerateConfig,
45    ) -> Result<Self, CacheError> {
46        let commands_hash = Self::hash_commands(commands)?;
47        let structs_hash = Self::hash_structs(structs)?;
48        let config_hash = Self::hash_config(config)?;
49        let combined_hash = Self::combine_hashes(&commands_hash, &structs_hash, &config_hash)?;
50
51        Ok(Self {
52            version: Self::CURRENT_VERSION,
53            commands_hash,
54            structs_hash,
55            config_hash,
56            combined_hash,
57        })
58    }
59
60    /// Load cache from file
61    pub fn load<P: AsRef<Path>>(output_dir: P) -> Result<Self, CacheError> {
62        let cache_path = Self::cache_path(output_dir);
63        let content = fs::read_to_string(cache_path)?;
64        let cache: Self = serde_json::from_str(&content)?;
65        Ok(cache)
66    }
67
68    /// Save cache to file
69    pub fn save<P: AsRef<Path>>(&self, output_dir: P) -> Result<(), CacheError> {
70        let cache_path = Self::cache_path(output_dir);
71
72        // Ensure output directory exists
73        if let Some(parent) = cache_path.parent() {
74            fs::create_dir_all(parent)?;
75        }
76
77        let content = serde_json::to_string_pretty(self)?;
78        fs::write(cache_path, content)?;
79        Ok(())
80    }
81
82    /// Check if generation is needed by comparing with previous cache
83    pub fn needs_regeneration<P: AsRef<Path>>(
84        output_dir: P,
85        commands: &[CommandInfo],
86        structs: &HashMap<String, StructInfo>,
87        config: &GenerateConfig,
88    ) -> Result<bool, CacheError> {
89        // Try to load previous cache
90        let previous_cache = match Self::load(&output_dir) {
91            Ok(cache) => cache,
92            Err(_) => {
93                // No cache file or error reading it - needs regeneration
94                return Ok(true);
95            }
96        };
97
98        // Check version compatibility
99        if previous_cache.version != Self::CURRENT_VERSION {
100            return Ok(true);
101        }
102
103        // Generate current cache
104        let current_cache = Self::new(commands, structs, config)?;
105
106        // Compare combined hashes
107        Ok(previous_cache.combined_hash != current_cache.combined_hash)
108    }
109
110    /// Get the cache file path
111    fn cache_path<P: AsRef<Path>>(output_dir: P) -> PathBuf {
112        output_dir.as_ref().join(CACHE_FILE_NAME)
113    }
114
115    /// Generate a deterministic hash of commands
116    fn hash_commands(commands: &[CommandInfo]) -> Result<String, CacheError> {
117        // Create a serializable representation
118        #[derive(Serialize)]
119        struct CommandHashData<'a> {
120            name: &'a str,
121            file_path: &'a str,
122            parameters: Vec<ParameterHashData<'a>>,
123            return_type: &'a str,
124            is_async: bool,
125            channels: Vec<ChannelHashData<'a>>,
126        }
127
128        #[derive(Serialize)]
129        struct ParameterHashData<'a> {
130            name: &'a str,
131            rust_type: &'a str,
132            is_optional: bool,
133        }
134
135        #[derive(Serialize)]
136        struct ChannelHashData<'a> {
137            parameter_name: &'a str,
138            message_type: &'a str,
139        }
140
141        let hash_data: Vec<CommandHashData> = commands
142            .iter()
143            .map(|cmd| CommandHashData {
144                name: &cmd.name,
145                file_path: &cmd.file_path,
146                parameters: cmd
147                    .parameters
148                    .iter()
149                    .map(|p| ParameterHashData {
150                        name: &p.name,
151                        rust_type: &p.rust_type,
152                        is_optional: p.is_optional,
153                    })
154                    .collect(),
155                return_type: &cmd.return_type,
156                is_async: cmd.is_async,
157                channels: cmd
158                    .channels
159                    .iter()
160                    .map(|c| ChannelHashData {
161                        parameter_name: &c.parameter_name,
162                        message_type: &c.message_type,
163                    })
164                    .collect(),
165            })
166            .collect();
167
168        let json = serde_json::to_string(&hash_data)?;
169        Ok(Self::compute_hash(&json))
170    }
171
172    /// Generate a deterministic hash of structs
173    fn hash_structs(structs: &HashMap<String, StructInfo>) -> Result<String, CacheError> {
174        #[derive(Serialize)]
175        struct StructHashData<'a> {
176            name: &'a str,
177            file_path: &'a str,
178            is_enum: bool,
179            fields: Vec<FieldHashData<'a>>,
180        }
181
182        #[derive(Serialize)]
183        struct FieldHashData<'a> {
184            name: &'a str,
185            rust_type: &'a str,
186            is_optional: bool,
187            is_public: bool,
188        }
189
190        // Sort by name for deterministic ordering
191        let mut sorted_structs: Vec<_> = structs.values().collect();
192        sorted_structs.sort_by(|a, b| a.name.cmp(&b.name));
193
194        let hash_data: Vec<StructHashData> = sorted_structs
195            .iter()
196            .map(|s| StructHashData {
197                name: &s.name,
198                file_path: &s.file_path,
199                is_enum: s.is_enum,
200                fields: s
201                    .fields
202                    .iter()
203                    .map(|f| FieldHashData {
204                        name: &f.name,
205                        rust_type: &f.rust_type,
206                        is_optional: f.is_optional,
207                        is_public: f.is_public,
208                    })
209                    .collect(),
210            })
211            .collect();
212
213        let json = serde_json::to_string(&hash_data)?;
214        Ok(Self::compute_hash(&json))
215    }
216
217    /// Generate a hash of configuration settings that affect output
218    fn hash_config(config: &GenerateConfig) -> Result<String, CacheError> {
219        #[derive(Serialize)]
220        struct ConfigHashData<'a> {
221            validation_library: &'a str,
222            include_private: bool,
223            type_mappings: Option<&'a HashMap<String, String>>,
224            default_parameter_case: &'a str,
225            default_field_case: &'a str,
226        }
227
228        let hash_data = ConfigHashData {
229            validation_library: &config.validation_library,
230            include_private: config.include_private.unwrap_or(false),
231            type_mappings: config.type_mappings.as_ref(),
232            default_parameter_case: &config.default_parameter_case,
233            default_field_case: &config.default_field_case,
234        };
235
236        let json = serde_json::to_string(&hash_data)?;
237        Ok(Self::compute_hash(&json))
238    }
239
240    /// Combine multiple hashes into a single hash
241    fn combine_hashes(commands: &str, structs: &str, config: &str) -> Result<String, CacheError> {
242        let combined = format!("{}{}{}", commands, structs, config);
243        Ok(Self::compute_hash(&combined))
244    }
245
246    /// Compute SHA-256 hash of a string
247    fn compute_hash(data: &str) -> String {
248        use std::collections::hash_map::DefaultHasher;
249        use std::hash::{Hash, Hasher};
250
251        let mut hasher = DefaultHasher::new();
252        data.hash(&mut hasher);
253        format!("{:x}", hasher.finish())
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    // Test utilities already imported from parent module
261    use tempfile::TempDir;
262
263    fn create_test_config() -> GenerateConfig {
264        GenerateConfig {
265            project_path: "./src-tauri".to_string(),
266            output_path: "./src/generated".to_string(),
267            validation_library: "none".to_string(),
268            verbose: Some(false),
269            visualize_deps: Some(false),
270            include_private: Some(false),
271            type_mappings: None,
272            exclude_patterns: None,
273            include_patterns: None,
274            default_parameter_case: "camelCase".to_string(),
275            default_field_case: "snake_case".to_string(),
276            force: Some(false),
277        }
278    }
279
280    fn create_test_command(name: &str) -> CommandInfo {
281        CommandInfo::new_for_test(name, "test.rs", 1, vec![], "String", false, vec![])
282    }
283
284    #[test]
285    fn test_cache_creation() {
286        let commands = vec![create_test_command("test_command")];
287        let structs = HashMap::new();
288        let config = create_test_config();
289
290        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
291
292        assert_eq!(cache.version, GenerationCache::CURRENT_VERSION);
293        assert!(!cache.commands_hash.is_empty());
294        assert!(!cache.structs_hash.is_empty());
295        assert!(!cache.config_hash.is_empty());
296        assert!(!cache.combined_hash.is_empty());
297    }
298
299    #[test]
300    fn test_cache_save_and_load() {
301        let temp_dir = TempDir::new().unwrap();
302        let commands = vec![create_test_command("test_command")];
303        let structs = HashMap::new();
304        let config = create_test_config();
305
306        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
307        cache.save(temp_dir.path()).unwrap();
308
309        let loaded_cache = GenerationCache::load(temp_dir.path()).unwrap();
310
311        assert_eq!(cache.combined_hash, loaded_cache.combined_hash);
312        assert_eq!(cache.commands_hash, loaded_cache.commands_hash);
313        assert_eq!(cache.structs_hash, loaded_cache.structs_hash);
314    }
315
316    #[test]
317    fn test_needs_regeneration_no_cache() {
318        let temp_dir = TempDir::new().unwrap();
319        let commands = vec![create_test_command("test_command")];
320        let structs = HashMap::new();
321        let config = create_test_config();
322
323        let needs_regen =
324            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &config)
325                .unwrap();
326
327        assert!(needs_regen);
328    }
329
330    #[test]
331    fn test_needs_regeneration_same_state() {
332        let temp_dir = TempDir::new().unwrap();
333        let commands = vec![create_test_command("test_command")];
334        let structs = HashMap::new();
335        let config = create_test_config();
336
337        // Save initial cache
338        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
339        cache.save(temp_dir.path()).unwrap();
340
341        // Check if regeneration needed with same data
342        let needs_regen =
343            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &config)
344                .unwrap();
345
346        assert!(!needs_regen);
347    }
348
349    #[test]
350    fn test_needs_regeneration_command_changed() {
351        let temp_dir = TempDir::new().unwrap();
352        let commands = vec![create_test_command("test_command")];
353        let structs = HashMap::new();
354        let config = create_test_config();
355
356        // Save initial cache
357        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
358        cache.save(temp_dir.path()).unwrap();
359
360        // Change commands
361        let new_commands = vec![create_test_command("different_command")];
362
363        let needs_regen =
364            GenerationCache::needs_regeneration(temp_dir.path(), &new_commands, &structs, &config)
365                .unwrap();
366
367        assert!(needs_regen);
368    }
369
370    #[test]
371    fn test_needs_regeneration_config_changed() {
372        let temp_dir = TempDir::new().unwrap();
373        let commands = vec![create_test_command("test_command")];
374        let structs = HashMap::new();
375        let config = create_test_config();
376
377        // Save initial cache
378        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
379        cache.save(temp_dir.path()).unwrap();
380
381        // Change config
382        let mut new_config = config;
383        new_config.validation_library = "zod".to_string();
384
385        let needs_regen =
386            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &new_config)
387                .unwrap();
388
389        assert!(needs_regen);
390    }
391
392    #[test]
393    fn test_hash_determinism() {
394        let commands = vec![create_test_command("test_command")];
395        let structs = HashMap::new();
396        let config = create_test_config();
397
398        let cache1 = GenerationCache::new(&commands, &structs, &config).unwrap();
399        let cache2 = GenerationCache::new(&commands, &structs, &config).unwrap();
400
401        assert_eq!(cache1.combined_hash, cache2.combined_hash);
402        assert_eq!(cache1.commands_hash, cache2.commands_hash);
403        assert_eq!(cache1.structs_hash, cache2.structs_hash);
404        assert_eq!(cache1.config_hash, cache2.config_hash);
405    }
406
407    #[test]
408    fn test_needs_regeneration_version_mismatch() {
409        let temp_dir = TempDir::new().unwrap();
410        let commands = vec![create_test_command("test_command")];
411        let structs = HashMap::new();
412        let config = create_test_config();
413
414        // Create a cache with a different version
415        let old_cache_content = r#"{
416            "version": 0,
417            "commands_hash": "abc123",
418            "structs_hash": "def456",
419            "config_hash": "ghi789",
420            "combined_hash": "xyz000"
421        }"#;
422        let cache_path = temp_dir.path().join(".typecache");
423        std::fs::write(&cache_path, old_cache_content).unwrap();
424
425        // Should need regeneration due to version mismatch
426        let needs_regen =
427            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &config)
428                .unwrap();
429
430        assert!(needs_regen);
431    }
432
433    #[test]
434    fn test_empty_commands_and_structs() {
435        let commands: Vec<CommandInfo> = vec![];
436        let structs: HashMap<String, crate::models::StructInfo> = HashMap::new();
437        let config = create_test_config();
438
439        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
440
441        // Should still create valid hashes even with empty data
442        assert!(!cache.commands_hash.is_empty());
443        assert!(!cache.structs_hash.is_empty());
444        assert!(!cache.combined_hash.is_empty());
445    }
446
447    #[test]
448    fn test_struct_hash_order_independence() {
449        use crate::models::{FieldInfo, StructInfo, TypeStructure};
450
451        let config = create_test_config();
452        let commands = vec![create_test_command("test_command")];
453
454        // Create two structs
455        let struct_a = StructInfo {
456            name: "StructA".to_string(),
457            fields: vec![FieldInfo {
458                name: "field_a".to_string(),
459                rust_type: "String".to_string(),
460                is_optional: false,
461                is_public: true,
462                validator_attributes: None,
463                serde_rename: None,
464                type_structure: TypeStructure::Primitive("string".to_string()),
465            }],
466            file_path: "test.rs".to_string(),
467            is_enum: false,
468            serde_rename_all: None,
469            serde_tag: None,
470            enum_variants: None,
471        };
472
473        let struct_b = StructInfo {
474            name: "StructB".to_string(),
475            fields: vec![FieldInfo {
476                name: "field_b".to_string(),
477                rust_type: "i32".to_string(),
478                is_optional: false,
479                is_public: true,
480                validator_attributes: None,
481                serde_rename: None,
482                type_structure: TypeStructure::Primitive("number".to_string()),
483            }],
484            file_path: "test.rs".to_string(),
485            is_enum: false,
486            serde_rename_all: None,
487            serde_tag: None,
488            enum_variants: None,
489        };
490
491        // Insert in order A, B
492        let mut structs1 = HashMap::new();
493        structs1.insert("StructA".to_string(), struct_a.clone());
494        structs1.insert("StructB".to_string(), struct_b.clone());
495
496        // Insert in order B, A (reverse)
497        let mut structs2 = HashMap::new();
498        structs2.insert("StructB".to_string(), struct_b);
499        structs2.insert("StructA".to_string(), struct_a);
500
501        let cache1 = GenerationCache::new(&commands, &structs1, &config).unwrap();
502        let cache2 = GenerationCache::new(&commands, &structs2, &config).unwrap();
503
504        // Hash should be the same regardless of insertion order
505        assert_eq!(cache1.structs_hash, cache2.structs_hash);
506        assert_eq!(cache1.combined_hash, cache2.combined_hash);
507    }
508
509    #[test]
510    fn test_needs_regeneration_with_corrupted_cache_file() {
511        let temp_dir = TempDir::new().unwrap();
512        let commands = vec![create_test_command("test_command")];
513        let structs = HashMap::new();
514        let config = create_test_config();
515
516        // Create a corrupted cache file
517        let cache_path = temp_dir.path().join(".typecache");
518        std::fs::write(&cache_path, "not valid json").unwrap();
519
520        // Should need regeneration because cache is unreadable
521        let needs_regen =
522            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &config)
523                .unwrap();
524
525        assert!(needs_regen);
526    }
527
528    #[test]
529    fn test_cache_with_type_mappings_config() {
530        let commands = vec![create_test_command("test_command")];
531        let structs = HashMap::new();
532
533        let mut config1 = create_test_config();
534        let mut type_mappings = std::collections::HashMap::new();
535        type_mappings.insert("CustomType".to_string(), "string".to_string());
536        config1.type_mappings = Some(type_mappings);
537
538        let config2 = create_test_config(); // No type mappings
539
540        let cache1 = GenerationCache::new(&commands, &structs, &config1).unwrap();
541        let cache2 = GenerationCache::new(&commands, &structs, &config2).unwrap();
542
543        // Config hash should differ when type_mappings differ
544        assert_ne!(cache1.config_hash, cache2.config_hash);
545        assert_ne!(cache1.combined_hash, cache2.combined_hash);
546    }
547
548    #[test]
549    fn test_cache_with_channels() {
550        use crate::models::ChannelInfo;
551
552        let structs = HashMap::new();
553        let config = create_test_config();
554
555        let channel = ChannelInfo::new_for_test("progress", "u32", "test_command", "test.rs", 1);
556
557        let cmd_with_channel = CommandInfo::new_for_test(
558            "test_command",
559            "test.rs",
560            1,
561            vec![],
562            "String",
563            false,
564            vec![channel],
565        );
566
567        let cmd_without_channel = create_test_command("test_command");
568
569        let cache_with = GenerationCache::new(&[cmd_with_channel], &structs, &config).unwrap();
570        let cache_without =
571            GenerationCache::new(&[cmd_without_channel], &structs, &config).unwrap();
572
573        // Commands hash should differ when channels differ
574        assert_ne!(cache_with.commands_hash, cache_without.commands_hash);
575    }
576
577    #[test]
578    fn test_save_creates_output_directory() {
579        let temp_dir = TempDir::new().unwrap();
580        let nested_output = temp_dir.path().join("nested").join("output").join("dir");
581
582        let commands = vec![create_test_command("test_command")];
583        let structs = HashMap::new();
584        let config = create_test_config();
585
586        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
587
588        // Should create nested directories
589        cache.save(&nested_output).unwrap();
590
591        assert!(nested_output.join(".typecache").exists());
592    }
593
594    #[test]
595    fn test_load_nonexistent_cache() {
596        let temp_dir = TempDir::new().unwrap();
597
598        // Should return an error when cache doesn't exist
599        let result = GenerationCache::load(temp_dir.path());
600        assert!(result.is_err());
601    }
602}