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 #[serde(default = "default_project_path")]
22 pub project_path: String,
23
24 #[serde(default = "default_output_path")]
26 pub output_path: String,
27
28 #[serde(default = "default_validation_library")]
30 pub validation_library: String,
31
32 #[serde(default)]
34 pub verbose: Option<bool>,
35
36 #[serde(default)]
38 pub visualize_deps: Option<bool>,
39
40 #[serde(default)]
42 pub include_private: Option<bool>,
43
44 #[serde(default)]
46 pub type_mappings: Option<std::collections::HashMap<String, String>>,
47
48 #[serde(default)]
50 pub exclude_patterns: Option<Vec<String>>,
51
52 #[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 pub fn new() -> Self {
88 Self::default()
89 }
90
91 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 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 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 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 pub fn save_to_tauri_config<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
171 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 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 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 if !tauri_obj.contains_key("plugins") {
204 tauri_obj.insert("plugins".to_string(), serde_json::json!({}));
205 }
206
207 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 pub fn validate(&self) -> Result<(), ConfigError> {
221 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 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 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 pub fn is_verbose(&self) -> bool {
276 self.verbose.unwrap_or(false)
277 }
278
279 pub fn should_visualize_deps(&self) -> bool {
281 self.visualize_deps.unwrap_or(false)
282 }
283
284 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"); }
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 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 config.save_to_tauri_config(&tauri_conf_path).unwrap();
400
401 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 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 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 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 config.save_to_tauri_config(&tauri_conf_path).unwrap();
456
457 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 assert_eq!(updated_json["package"]["productName"], "My App");
463 assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
464
465 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}