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 #[serde(default = "default_parameter_case")]
60 pub default_parameter_case: String,
61
62 #[serde(default = "default_field_case")]
67 pub default_field_case: String,
68}
69
70fn default_project_path() -> String {
71 "./src-tauri".to_string()
72}
73
74fn default_output_path() -> String {
75 "./src/generated".to_string()
76}
77
78fn default_validation_library() -> String {
79 "none".to_string()
80}
81
82fn default_parameter_case() -> String {
83 "camelCase".to_string()
84}
85
86fn default_field_case() -> String {
87 "snake_case".to_string()
90}
91
92impl Default for GenerateConfig {
93 fn default() -> Self {
94 Self {
95 project_path: default_project_path(),
96 output_path: default_output_path(),
97 validation_library: default_validation_library(),
98 verbose: Some(false),
99 visualize_deps: Some(false),
100 include_private: Some(false),
101 type_mappings: None,
102 exclude_patterns: None,
103 include_patterns: None,
104 default_parameter_case: default_parameter_case(),
105 default_field_case: default_field_case(),
106 }
107 }
108}
109
110impl GenerateConfig {
111 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
118 let content = fs::read_to_string(path)?;
119 let config: Self = serde_json::from_str(&content)?;
120 config.validate()?;
121 Ok(config)
122 }
123
124 pub fn from_tauri_config<P: AsRef<Path>>(path: P) -> Result<Option<Self>, ConfigError> {
126 let content = fs::read_to_string(path)?;
127 let tauri_config: serde_json::Value = serde_json::from_str(&content)?;
128
129 if let Some(plugins) = tauri_config.get("plugins") {
131 if let Some(typegen) = plugins.get("typegen") {
132 let mut config = Self::default();
133
134 if let Some(project_path) = typegen.get("projectPath").and_then(|v| v.as_str()) {
135 config.project_path = project_path.to_string();
136 }
137 if let Some(output_path) = typegen.get("outputPath").and_then(|v| v.as_str()) {
138 config.output_path = output_path.to_string();
139 }
140 if let Some(validation) = typegen.get("validationLibrary").and_then(|v| v.as_str())
141 {
142 config.validation_library = validation.to_string();
143 }
144 if let Some(verbose) = typegen.get("verbose").and_then(|v| v.as_bool()) {
145 config.verbose = Some(verbose);
146 }
147 if let Some(visualize_deps) = typegen.get("visualizeDeps").and_then(|v| v.as_bool())
148 {
149 config.visualize_deps = Some(visualize_deps);
150 }
151 if let Some(include_private) =
152 typegen.get("includePrivate").and_then(|v| v.as_bool())
153 {
154 config.include_private = Some(include_private);
155 }
156 if let Some(type_mappings) = typegen.get("typeMappings") {
157 if let Ok(mappings) = serde_json::from_value::<
158 std::collections::HashMap<String, String>,
159 >(type_mappings.clone())
160 {
161 config.type_mappings = Some(mappings);
162 }
163 }
164 if let Some(exclude_patterns) = typegen.get("excludePatterns") {
165 if let Ok(patterns) =
166 serde_json::from_value::<Vec<String>>(exclude_patterns.clone())
167 {
168 config.exclude_patterns = Some(patterns);
169 }
170 }
171 if let Some(include_patterns) = typegen.get("includePatterns") {
172 if let Ok(patterns) =
173 serde_json::from_value::<Vec<String>>(include_patterns.clone())
174 {
175 config.include_patterns = Some(patterns);
176 }
177 }
178
179 config.validate()?;
180 return Ok(Some(config));
181 }
182 }
183
184 Ok(None)
185 }
186
187 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
189 let content = serde_json::to_string_pretty(self)?;
190 fs::write(path, content)?;
191 Ok(())
192 }
193
194 pub fn save_to_tauri_config<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
196 if !path.as_ref().exists() {
198 return Err(ConfigError::InvalidConfig(format!(
199 "tauri.conf.json not found at {}. Please ensure you have a Tauri project initialized.",
200 path.as_ref().display()
201 )));
202 }
203
204 let content = fs::read_to_string(&path)?;
205 let mut tauri_config = serde_json::from_str::<serde_json::Value>(&content)?;
206
207 let typegen_config = serde_json::json!({
209 "projectPath": self.project_path,
210 "outputPath": self.output_path,
211 "validationLibrary": self.validation_library,
212 "verbose": self.verbose.unwrap_or(false),
213 "visualizeDeps": self.visualize_deps.unwrap_or(false),
214 "includePrivate": self.include_private.unwrap_or(false),
215 "typeMappings": self.type_mappings,
216 "excludePatterns": self.exclude_patterns,
217 "includePatterns": self.include_patterns,
218 });
219
220 if !tauri_config.is_object() {
222 tauri_config = serde_json::json!({});
223 }
224
225 let tauri_obj = tauri_config.as_object_mut().unwrap();
226
227 if !tauri_obj.contains_key("plugins") {
229 tauri_obj.insert("plugins".to_string(), serde_json::json!({}));
230 }
231
232 if let Some(plugins) = tauri_obj.get_mut("plugins") {
234 if let Some(plugins_obj) = plugins.as_object_mut() {
235 plugins_obj.insert("typegen".to_string(), typegen_config);
236 }
237 }
238
239 let content = serde_json::to_string_pretty(&tauri_config)?;
240 fs::write(path, content)?;
241 Ok(())
242 }
243
244 pub fn validate(&self) -> Result<(), ConfigError> {
246 match self.validation_library.as_str() {
248 "zod" | "none" => {}
249 _ => {
250 return Err(ConfigError::InvalidValidationLibrary(
251 self.validation_library.clone(),
252 ));
253 }
254 }
255
256 let project_path = Path::new(&self.project_path);
258 if !project_path.exists() {
259 return Err(ConfigError::InvalidConfig(format!(
260 "Project path does not exist: {}",
261 self.project_path
262 )));
263 }
264
265 Ok(())
266 }
267
268 pub fn merge(&mut self, other: &GenerateConfig) {
270 if other.project_path != default_project_path() {
271 self.project_path = other.project_path.clone();
272 }
273 if other.output_path != default_output_path() {
274 self.output_path = other.output_path.clone();
275 }
276 if other.validation_library != default_validation_library() {
277 self.validation_library = other.validation_library.clone();
278 }
279 if other.verbose.is_some() {
280 self.verbose = other.verbose;
281 }
282 if other.visualize_deps.is_some() {
283 self.visualize_deps = other.visualize_deps;
284 }
285 if other.include_private.is_some() {
286 self.include_private = other.include_private;
287 }
288 if other.type_mappings.is_some() {
289 self.type_mappings = other.type_mappings.clone();
290 }
291 if other.exclude_patterns.is_some() {
292 self.exclude_patterns = other.exclude_patterns.clone();
293 }
294 if other.include_patterns.is_some() {
295 self.include_patterns = other.include_patterns.clone();
296 }
297 }
298
299 pub fn is_verbose(&self) -> bool {
301 self.verbose.unwrap_or(false)
302 }
303
304 pub fn should_visualize_deps(&self) -> bool {
306 self.visualize_deps.unwrap_or(false)
307 }
308
309 pub fn should_include_private(&self) -> bool {
311 self.include_private.unwrap_or(false)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use tempfile::NamedTempFile;
319
320 #[test]
321 fn test_default_config() {
322 let config = GenerateConfig::default();
323 assert_eq!(config.project_path, "./src-tauri");
324 assert_eq!(config.output_path, "./src/generated");
325 assert_eq!(config.validation_library, "none");
326 assert!(!config.is_verbose());
327 assert!(!config.should_visualize_deps());
328 assert!(!config.should_include_private());
329 }
330
331 #[test]
332 fn test_config_validation() {
333 let config = GenerateConfig {
334 validation_library: "invalid".to_string(),
335 ..Default::default()
336 };
337
338 let result = config.validate();
339 assert!(result.is_err());
340 if let Err(ConfigError::InvalidValidationLibrary(lib)) = result {
341 assert_eq!(lib, "invalid");
342 } else {
343 panic!("Expected InvalidValidationLibrary error");
344 }
345 }
346
347 #[test]
348 fn test_config_merge() {
349 let mut base = GenerateConfig::default();
350 let override_config = GenerateConfig {
351 output_path: "./custom".to_string(),
352 verbose: Some(true),
353 ..Default::default()
354 };
355
356 base.merge(&override_config);
357 assert_eq!(base.output_path, "./custom");
358 assert!(base.is_verbose());
359 assert_eq!(base.validation_library, "none"); }
361
362 #[test]
363 fn test_save_and_load_config() {
364 let temp_dir = tempfile::TempDir::new().unwrap();
365 let project_path = temp_dir.path().join("src-tauri");
366 std::fs::create_dir_all(&project_path).unwrap();
367
368 let config = GenerateConfig {
369 project_path: project_path.to_string_lossy().to_string(),
370 output_path: "./test".to_string(),
371 verbose: Some(true),
372 ..Default::default()
373 };
374
375 let temp_file = NamedTempFile::new().unwrap();
376 config.save_to_file(temp_file.path()).unwrap();
377
378 let loaded_config = GenerateConfig::from_file(temp_file.path()).unwrap();
379 assert_eq!(loaded_config.output_path, "./test");
380 assert!(loaded_config.is_verbose());
381 }
382
383 #[test]
384 fn test_save_to_tauri_config_preserves_existing_content() {
385 let temp_dir = tempfile::TempDir::new().unwrap();
386 let project_path = temp_dir.path().join("src-tauri");
387 std::fs::create_dir_all(&project_path).unwrap();
388
389 let tauri_conf_path = temp_dir.path().join("tauri.conf.json");
390
391 let existing_content = serde_json::json!({
393 "package": {
394 "productName": "My App",
395 "version": "1.0.0"
396 },
397 "tauri": {
398 "allowlist": {
399 "all": false
400 }
401 },
402 "plugins": {
403 "shell": {
404 "all": false
405 }
406 }
407 });
408
409 fs::write(
410 &tauri_conf_path,
411 serde_json::to_string_pretty(&existing_content).unwrap(),
412 )
413 .unwrap();
414
415 let config = GenerateConfig {
416 project_path: project_path.to_string_lossy().to_string(),
417 output_path: "./test".to_string(),
418 validation_library: "zod".to_string(),
419 verbose: Some(true),
420 ..Default::default()
421 };
422
423 config.save_to_tauri_config(&tauri_conf_path).unwrap();
425
426 let updated_content = fs::read_to_string(&tauri_conf_path).unwrap();
428 let updated_json: serde_json::Value = serde_json::from_str(&updated_content).unwrap();
429
430 assert_eq!(updated_json["package"]["productName"], "My App");
432 assert_eq!(updated_json["package"]["version"], "1.0.0");
433 assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
434 assert_eq!(updated_json["plugins"]["shell"]["all"], false);
435
436 assert_eq!(updated_json["plugins"]["typegen"]["outputPath"], "./test");
438 assert_eq!(
439 updated_json["plugins"]["typegen"]["validationLibrary"],
440 "zod"
441 );
442 assert_eq!(updated_json["plugins"]["typegen"]["verbose"], true);
443 }
444
445 #[test]
446 fn test_save_to_tauri_config_creates_plugins_section() {
447 let temp_dir = tempfile::TempDir::new().unwrap();
448 let project_path = temp_dir.path().join("src-tauri");
449 std::fs::create_dir_all(&project_path).unwrap();
450
451 let tauri_conf_path = temp_dir.path().join("tauri.conf.json");
452
453 let existing_content = serde_json::json!({
455 "package": {
456 "productName": "My App",
457 "version": "1.0.0"
458 },
459 "tauri": {
460 "allowlist": {
461 "all": false
462 }
463 }
464 });
465
466 fs::write(
467 &tauri_conf_path,
468 serde_json::to_string_pretty(&existing_content).unwrap(),
469 )
470 .unwrap();
471
472 let config = GenerateConfig {
473 project_path: project_path.to_string_lossy().to_string(),
474 output_path: "./test".to_string(),
475 validation_library: "none".to_string(),
476 ..Default::default()
477 };
478
479 config.save_to_tauri_config(&tauri_conf_path).unwrap();
481
482 let updated_content = fs::read_to_string(&tauri_conf_path).unwrap();
484 let updated_json: serde_json::Value = serde_json::from_str(&updated_content).unwrap();
485
486 assert_eq!(updated_json["package"]["productName"], "My App");
488 assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
489
490 assert!(updated_json["plugins"].is_object());
492 assert_eq!(updated_json["plugins"]["typegen"]["outputPath"], "./test");
493 assert_eq!(
494 updated_json["plugins"]["typegen"]["validationLibrary"],
495 "none"
496 );
497 }
498}