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