tauri_typegen/interface/
config.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::Path;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum ConfigError {
8    #[error("IO error: {0}")]
9    Io(#[from] std::io::Error),
10    #[error("JSON parsing error: {0}")]
11    Json(#[from] serde_json::Error),
12    #[error("Invalid validation library: {0}. Use 'zod' or 'none'")]
13    InvalidValidationLibrary(String),
14    #[error("Invalid configuration: {0}")]
15    InvalidConfig(String),
16}
17
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct GenerateConfig {
20    /// Path to the Tauri project source directory
21    #[serde(default = "default_project_path")]
22    pub project_path: String,
23
24    /// Output path for generated TypeScript files
25    #[serde(default = "default_output_path")]
26    pub output_path: String,
27
28    /// Validation library to use ('zod' or 'none')
29    #[serde(default = "default_validation_library")]
30    pub validation_library: String,
31
32    /// Enable verbose output
33    #[serde(default)]
34    pub verbose: Option<bool>,
35
36    /// Generate dependency graph visualization
37    #[serde(default)]
38    pub visualize_deps: Option<bool>,
39
40    /// Include private struct fields in generation
41    #[serde(default)]
42    pub include_private: Option<bool>,
43
44    /// Custom type mappings
45    #[serde(default)]
46    pub type_mappings: Option<std::collections::HashMap<String, String>>,
47
48    /// File patterns to exclude from analysis
49    #[serde(default)]
50    pub exclude_patterns: Option<Vec<String>>,
51
52    /// File patterns to include in analysis (overrides excludes)
53    #[serde(default)]
54    pub include_patterns: Option<Vec<String>>,
55}
56
57fn default_project_path() -> String {
58    "./src-tauri".to_string()
59}
60
61fn default_output_path() -> String {
62    "./src/generated".to_string()
63}
64
65fn default_validation_library() -> String {
66    "none".to_string()
67}
68
69impl Default for GenerateConfig {
70    fn default() -> Self {
71        Self {
72            project_path: default_project_path(),
73            output_path: default_output_path(),
74            validation_library: default_validation_library(),
75            verbose: Some(false),
76            visualize_deps: Some(false),
77            include_private: Some(false),
78            type_mappings: None,
79            exclude_patterns: None,
80            include_patterns: None,
81        }
82    }
83}
84
85impl GenerateConfig {
86    /// Create a new configuration with defaults
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Load configuration from a file
92    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
93        let content = fs::read_to_string(path)?;
94        let config: Self = serde_json::from_str(&content)?;
95        config.validate()?;
96        Ok(config)
97    }
98
99    /// Load configuration from Tauri configuration file
100    pub fn from_tauri_config<P: AsRef<Path>>(path: P) -> Result<Option<Self>, ConfigError> {
101        let content = fs::read_to_string(path)?;
102        let tauri_config: serde_json::Value = serde_json::from_str(&content)?;
103
104        // Look for typegen plugin configuration
105        if let Some(plugins) = tauri_config.get("plugins") {
106            if let Some(typegen) = plugins.get("typegen") {
107                let mut config = Self::default();
108
109                if let Some(project_path) = typegen.get("projectPath").and_then(|v| v.as_str()) {
110                    config.project_path = project_path.to_string();
111                }
112                if let Some(output_path) = typegen.get("outputPath").and_then(|v| v.as_str()) {
113                    config.output_path = output_path.to_string();
114                }
115                if let Some(validation) = typegen.get("validationLibrary").and_then(|v| v.as_str())
116                {
117                    config.validation_library = validation.to_string();
118                }
119                if let Some(verbose) = typegen.get("verbose").and_then(|v| v.as_bool()) {
120                    config.verbose = Some(verbose);
121                }
122                if let Some(visualize_deps) = typegen.get("visualizeDeps").and_then(|v| v.as_bool())
123                {
124                    config.visualize_deps = Some(visualize_deps);
125                }
126                if let Some(include_private) =
127                    typegen.get("includePrivate").and_then(|v| v.as_bool())
128                {
129                    config.include_private = Some(include_private);
130                }
131                if let Some(type_mappings) = typegen.get("typeMappings") {
132                    if let Ok(mappings) = serde_json::from_value::<
133                        std::collections::HashMap<String, String>,
134                    >(type_mappings.clone())
135                    {
136                        config.type_mappings = Some(mappings);
137                    }
138                }
139                if let Some(exclude_patterns) = typegen.get("excludePatterns") {
140                    if let Ok(patterns) =
141                        serde_json::from_value::<Vec<String>>(exclude_patterns.clone())
142                    {
143                        config.exclude_patterns = Some(patterns);
144                    }
145                }
146                if let Some(include_patterns) = typegen.get("includePatterns") {
147                    if let Ok(patterns) =
148                        serde_json::from_value::<Vec<String>>(include_patterns.clone())
149                    {
150                        config.include_patterns = Some(patterns);
151                    }
152                }
153
154                config.validate()?;
155                return Ok(Some(config));
156            }
157        }
158
159        Ok(None)
160    }
161
162    /// Save configuration to a file
163    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
164        let content = serde_json::to_string_pretty(self)?;
165        fs::write(path, content)?;
166        Ok(())
167    }
168
169    /// Save configuration to Tauri configuration file
170    pub fn save_to_tauri_config<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
171        // Read existing tauri.conf.json - we require it to exist
172        if !path.as_ref().exists() {
173            return Err(ConfigError::InvalidConfig(format!(
174                "tauri.conf.json not found at {}. Please ensure you have a Tauri project initialized.",
175                path.as_ref().display()
176            )));
177        }
178
179        let content = fs::read_to_string(&path)?;
180        let mut tauri_config = serde_json::from_str::<serde_json::Value>(&content)?;
181
182        // Create typegen plugin configuration
183        let typegen_config = serde_json::json!({
184            "projectPath": self.project_path,
185            "outputPath": self.output_path,
186            "validationLibrary": self.validation_library,
187            "verbose": self.verbose.unwrap_or(false),
188            "visualizeDeps": self.visualize_deps.unwrap_or(false),
189            "includePrivate": self.include_private.unwrap_or(false),
190            "typeMappings": self.type_mappings,
191            "excludePatterns": self.exclude_patterns,
192            "includePatterns": self.include_patterns,
193        });
194
195        // Ensure plugins section exists and insert typegen configuration
196        if !tauri_config.is_object() {
197            tauri_config = serde_json::json!({});
198        }
199
200        let tauri_obj = tauri_config.as_object_mut().unwrap();
201
202        // Create plugins section if it doesn't exist
203        if !tauri_obj.contains_key("plugins") {
204            tauri_obj.insert("plugins".to_string(), serde_json::json!({}));
205        }
206
207        // Insert typegen configuration into plugins
208        if let Some(plugins) = tauri_obj.get_mut("plugins") {
209            if let Some(plugins_obj) = plugins.as_object_mut() {
210                plugins_obj.insert("typegen".to_string(), typegen_config);
211            }
212        }
213
214        let content = serde_json::to_string_pretty(&tauri_config)?;
215        fs::write(path, content)?;
216        Ok(())
217    }
218
219    /// Validate the configuration
220    pub fn validate(&self) -> Result<(), ConfigError> {
221        // Validate validation library
222        match self.validation_library.as_str() {
223            "zod" | "none" => {}
224            _ => {
225                return Err(ConfigError::InvalidValidationLibrary(
226                    self.validation_library.clone(),
227                ));
228            }
229        }
230
231        // Validate paths exist
232        let project_path = Path::new(&self.project_path);
233        if !project_path.exists() {
234            return Err(ConfigError::InvalidConfig(format!(
235                "Project path does not exist: {}",
236                self.project_path
237            )));
238        }
239
240        Ok(())
241    }
242
243    /// Merge with another configuration, with other taking precedence
244    pub fn merge(&mut self, other: &GenerateConfig) {
245        if other.project_path != default_project_path() {
246            self.project_path = other.project_path.clone();
247        }
248        if other.output_path != default_output_path() {
249            self.output_path = other.output_path.clone();
250        }
251        if other.validation_library != default_validation_library() {
252            self.validation_library = other.validation_library.clone();
253        }
254        if other.verbose.is_some() {
255            self.verbose = other.verbose;
256        }
257        if other.visualize_deps.is_some() {
258            self.visualize_deps = other.visualize_deps;
259        }
260        if other.include_private.is_some() {
261            self.include_private = other.include_private;
262        }
263        if other.type_mappings.is_some() {
264            self.type_mappings = other.type_mappings.clone();
265        }
266        if other.exclude_patterns.is_some() {
267            self.exclude_patterns = other.exclude_patterns.clone();
268        }
269        if other.include_patterns.is_some() {
270            self.include_patterns = other.include_patterns.clone();
271        }
272    }
273
274    /// Get effective verbose setting
275    pub fn is_verbose(&self) -> bool {
276        self.verbose.unwrap_or(false)
277    }
278
279    /// Get effective visualize_deps setting
280    pub fn should_visualize_deps(&self) -> bool {
281        self.visualize_deps.unwrap_or(false)
282    }
283
284    /// Get effective include_private setting
285    pub fn should_include_private(&self) -> bool {
286        self.include_private.unwrap_or(false)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use tempfile::NamedTempFile;
294
295    #[test]
296    fn test_default_config() {
297        let config = GenerateConfig::default();
298        assert_eq!(config.project_path, "./src-tauri");
299        assert_eq!(config.output_path, "./src/generated");
300        assert_eq!(config.validation_library, "none");
301        assert!(!config.is_verbose());
302        assert!(!config.should_visualize_deps());
303        assert!(!config.should_include_private());
304    }
305
306    #[test]
307    fn test_config_validation() {
308        let config = GenerateConfig {
309            validation_library: "invalid".to_string(),
310            ..Default::default()
311        };
312
313        let result = config.validate();
314        assert!(result.is_err());
315        if let Err(ConfigError::InvalidValidationLibrary(lib)) = result {
316            assert_eq!(lib, "invalid");
317        } else {
318            panic!("Expected InvalidValidationLibrary error");
319        }
320    }
321
322    #[test]
323    fn test_config_merge() {
324        let mut base = GenerateConfig::default();
325        let override_config = GenerateConfig {
326            output_path: "./custom".to_string(),
327            verbose: Some(true),
328            ..Default::default()
329        };
330
331        base.merge(&override_config);
332        assert_eq!(base.output_path, "./custom");
333        assert!(base.is_verbose());
334        assert_eq!(base.validation_library, "none"); // Should remain default
335    }
336
337    #[test]
338    fn test_save_and_load_config() {
339        let temp_dir = tempfile::TempDir::new().unwrap();
340        let project_path = temp_dir.path().join("src-tauri");
341        std::fs::create_dir_all(&project_path).unwrap();
342
343        let config = GenerateConfig {
344            project_path: project_path.to_string_lossy().to_string(),
345            output_path: "./test".to_string(),
346            verbose: Some(true),
347            ..Default::default()
348        };
349
350        let temp_file = NamedTempFile::new().unwrap();
351        config.save_to_file(temp_file.path()).unwrap();
352
353        let loaded_config = GenerateConfig::from_file(temp_file.path()).unwrap();
354        assert_eq!(loaded_config.output_path, "./test");
355        assert!(loaded_config.is_verbose());
356    }
357
358    #[test]
359    fn test_save_to_tauri_config_preserves_existing_content() {
360        let temp_dir = tempfile::TempDir::new().unwrap();
361        let project_path = temp_dir.path().join("src-tauri");
362        std::fs::create_dir_all(&project_path).unwrap();
363
364        let tauri_conf_path = temp_dir.path().join("tauri.conf.json");
365
366        // Create existing tauri.conf.json with some content
367        let existing_content = serde_json::json!({
368            "package": {
369                "productName": "My App",
370                "version": "1.0.0"
371            },
372            "tauri": {
373                "allowlist": {
374                    "all": false
375                }
376            },
377            "plugins": {
378                "shell": {
379                    "all": false
380                }
381            }
382        });
383
384        fs::write(
385            &tauri_conf_path,
386            serde_json::to_string_pretty(&existing_content).unwrap(),
387        )
388        .unwrap();
389
390        let config = GenerateConfig {
391            project_path: project_path.to_string_lossy().to_string(),
392            output_path: "./test".to_string(),
393            validation_library: "zod".to_string(),
394            verbose: Some(true),
395            ..Default::default()
396        };
397
398        // Save to tauri config - should preserve existing content
399        config.save_to_tauri_config(&tauri_conf_path).unwrap();
400
401        // Read back and verify
402        let updated_content = fs::read_to_string(&tauri_conf_path).unwrap();
403        let updated_json: serde_json::Value = serde_json::from_str(&updated_content).unwrap();
404
405        // Check that existing content is preserved
406        assert_eq!(updated_json["package"]["productName"], "My App");
407        assert_eq!(updated_json["package"]["version"], "1.0.0");
408        assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
409        assert_eq!(updated_json["plugins"]["shell"]["all"], false);
410
411        // Check that typegen config was added
412        assert_eq!(updated_json["plugins"]["typegen"]["outputPath"], "./test");
413        assert_eq!(
414            updated_json["plugins"]["typegen"]["validationLibrary"],
415            "zod"
416        );
417        assert_eq!(updated_json["plugins"]["typegen"]["verbose"], true);
418    }
419
420    #[test]
421    fn test_save_to_tauri_config_creates_plugins_section() {
422        let temp_dir = tempfile::TempDir::new().unwrap();
423        let project_path = temp_dir.path().join("src-tauri");
424        std::fs::create_dir_all(&project_path).unwrap();
425
426        let tauri_conf_path = temp_dir.path().join("tauri.conf.json");
427
428        // Create existing tauri.conf.json without plugins section
429        let existing_content = serde_json::json!({
430            "package": {
431                "productName": "My App",
432                "version": "1.0.0"
433            },
434            "tauri": {
435                "allowlist": {
436                    "all": false
437                }
438            }
439        });
440
441        fs::write(
442            &tauri_conf_path,
443            serde_json::to_string_pretty(&existing_content).unwrap(),
444        )
445        .unwrap();
446
447        let config = GenerateConfig {
448            project_path: project_path.to_string_lossy().to_string(),
449            output_path: "./test".to_string(),
450            validation_library: "none".to_string(),
451            ..Default::default()
452        };
453
454        // Save to tauri config - should create plugins section
455        config.save_to_tauri_config(&tauri_conf_path).unwrap();
456
457        // Read back and verify
458        let updated_content = fs::read_to_string(&tauri_conf_path).unwrap();
459        let updated_json: serde_json::Value = serde_json::from_str(&updated_content).unwrap();
460
461        // Check that existing content is preserved
462        assert_eq!(updated_json["package"]["productName"], "My App");
463        assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
464
465        // Check that plugins section was created with typegen config
466        assert!(updated_json["plugins"].is_object());
467        assert_eq!(updated_json["plugins"]["typegen"]["outputPath"], "./test");
468        assert_eq!(
469            updated_json["plugins"]["typegen"]["validationLibrary"],
470            "none"
471        );
472    }
473}