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        };
470
471        let struct_b = StructInfo {
472            name: "StructB".to_string(),
473            fields: vec![FieldInfo {
474                name: "field_b".to_string(),
475                rust_type: "i32".to_string(),
476                is_optional: false,
477                is_public: true,
478                validator_attributes: None,
479                serde_rename: None,
480                type_structure: TypeStructure::Primitive("number".to_string()),
481            }],
482            file_path: "test.rs".to_string(),
483            is_enum: false,
484            serde_rename_all: None,
485        };
486
487        // Insert in order A, B
488        let mut structs1 = HashMap::new();
489        structs1.insert("StructA".to_string(), struct_a.clone());
490        structs1.insert("StructB".to_string(), struct_b.clone());
491
492        // Insert in order B, A (reverse)
493        let mut structs2 = HashMap::new();
494        structs2.insert("StructB".to_string(), struct_b);
495        structs2.insert("StructA".to_string(), struct_a);
496
497        let cache1 = GenerationCache::new(&commands, &structs1, &config).unwrap();
498        let cache2 = GenerationCache::new(&commands, &structs2, &config).unwrap();
499
500        // Hash should be the same regardless of insertion order
501        assert_eq!(cache1.structs_hash, cache2.structs_hash);
502        assert_eq!(cache1.combined_hash, cache2.combined_hash);
503    }
504
505    #[test]
506    fn test_needs_regeneration_with_corrupted_cache_file() {
507        let temp_dir = TempDir::new().unwrap();
508        let commands = vec![create_test_command("test_command")];
509        let structs = HashMap::new();
510        let config = create_test_config();
511
512        // Create a corrupted cache file
513        let cache_path = temp_dir.path().join(".typecache");
514        std::fs::write(&cache_path, "not valid json").unwrap();
515
516        // Should need regeneration because cache is unreadable
517        let needs_regen =
518            GenerationCache::needs_regeneration(temp_dir.path(), &commands, &structs, &config)
519                .unwrap();
520
521        assert!(needs_regen);
522    }
523
524    #[test]
525    fn test_cache_with_type_mappings_config() {
526        let commands = vec![create_test_command("test_command")];
527        let structs = HashMap::new();
528
529        let mut config1 = create_test_config();
530        let mut type_mappings = std::collections::HashMap::new();
531        type_mappings.insert("CustomType".to_string(), "string".to_string());
532        config1.type_mappings = Some(type_mappings);
533
534        let config2 = create_test_config(); // No type mappings
535
536        let cache1 = GenerationCache::new(&commands, &structs, &config1).unwrap();
537        let cache2 = GenerationCache::new(&commands, &structs, &config2).unwrap();
538
539        // Config hash should differ when type_mappings differ
540        assert_ne!(cache1.config_hash, cache2.config_hash);
541        assert_ne!(cache1.combined_hash, cache2.combined_hash);
542    }
543
544    #[test]
545    fn test_cache_with_channels() {
546        use crate::models::ChannelInfo;
547
548        let structs = HashMap::new();
549        let config = create_test_config();
550
551        let channel = ChannelInfo::new_for_test("progress", "u32", "test_command", "test.rs", 1);
552
553        let cmd_with_channel = CommandInfo::new_for_test(
554            "test_command",
555            "test.rs",
556            1,
557            vec![],
558            "String",
559            false,
560            vec![channel],
561        );
562
563        let cmd_without_channel = create_test_command("test_command");
564
565        let cache_with = GenerationCache::new(&[cmd_with_channel], &structs, &config).unwrap();
566        let cache_without =
567            GenerationCache::new(&[cmd_without_channel], &structs, &config).unwrap();
568
569        // Commands hash should differ when channels differ
570        assert_ne!(cache_with.commands_hash, cache_without.commands_hash);
571    }
572
573    #[test]
574    fn test_save_creates_output_directory() {
575        let temp_dir = TempDir::new().unwrap();
576        let nested_output = temp_dir.path().join("nested").join("output").join("dir");
577
578        let commands = vec![create_test_command("test_command")];
579        let structs = HashMap::new();
580        let config = create_test_config();
581
582        let cache = GenerationCache::new(&commands, &structs, &config).unwrap();
583
584        // Should create nested directories
585        cache.save(&nested_output).unwrap();
586
587        assert!(nested_output.join(".typecache").exists());
588    }
589
590    #[test]
591    fn test_load_nonexistent_cache() {
592        let temp_dir = TempDir::new().unwrap();
593
594        // Should return an error when cache doesn't exist
595        let result = GenerationCache::load(temp_dir.path());
596        assert!(result.is_err());
597    }
598}