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 is_tauri_config = path
124 .as_ref()
125 .file_name()
126 .and_then(|n| n.to_str())
127 .map(|n| n == "tauri.conf.json")
128 .unwrap_or(false);
129
130 if is_tauri_config {
131 Self::from_tauri_config(path).and_then(|opt| {
132 opt.ok_or_else(|| {
133 ConfigError::InvalidConfig(
134 "No tauri-typegen plugin configuration found in tauri.conf.json"
135 .to_string(),
136 )
137 })
138 })
139 } else {
140 let content = fs::read_to_string(path)?;
141 let config: Self = serde_json::from_str(&content)?;
142 Ok(config)
143 }
144 }
145
146 pub fn from_tauri_config<P: AsRef<Path>>(path: P) -> Result<Option<Self>, ConfigError> {
148 let content = fs::read_to_string(path)?;
149 let tauri_config: serde_json::Value = serde_json::from_str(&content)?;
150
151 if let Some(plugins) = tauri_config.get("plugins") {
153 if let Some(typegen) = plugins.get("typegen") {
154 let mut config = Self::default();
155
156 let get_string = |keys: &[&str]| {
157 for key in keys {
158 if let Some(val) = typegen.get(*key).and_then(|v| v.as_str()) {
159 return Some(val.to_string());
160 }
161 }
162 None
163 };
164
165 let get_bool = |keys: &[&str]| {
166 for key in keys {
167 if let Some(val) = typegen.get(*key).and_then(|v| v.as_bool()) {
168 return Some(val);
169 }
170 }
171 None
172 };
173
174 if let Some(p) = get_string(&["projectPath", "project_path"]) {
175 config.project_path = p;
176 }
177 if let Some(o) = get_string(&[
178 "outputPath",
179 "output_path",
180 "generatedPath",
181 "generated_path",
182 ]) {
183 config.output_path = o;
184 }
185 if let Some(v) = get_string(&["validationLibrary", "validation_library"]) {
186 config.validation_library = v;
187 }
188 if let Some(v) = get_bool(&["verbose"]) {
189 config.verbose = Some(v);
190 }
191 if let Some(v) = get_bool(&["visualizeDeps", "visualize_deps"]) {
192 config.visualize_deps = Some(v);
193 }
194 if let Some(v) = get_bool(&["includePrivate", "include_private"]) {
195 config.include_private = Some(v);
196 }
197 if let Some(type_mappings) = typegen
198 .get("typeMappings")
199 .or_else(|| typegen.get("type_mappings"))
200 {
201 if let Ok(mappings) = serde_json::from_value::<
202 std::collections::HashMap<String, String>,
203 >(type_mappings.clone())
204 {
205 config.type_mappings = Some(mappings);
206 }
207 }
208 if let Some(exclude_patterns) = typegen
209 .get("excludePatterns")
210 .or_else(|| typegen.get("exclude_patterns"))
211 {
212 if let Ok(patterns) =
213 serde_json::from_value::<Vec<String>>(exclude_patterns.clone())
214 {
215 config.exclude_patterns = Some(patterns);
216 }
217 }
218 if let Some(include_patterns) = typegen
219 .get("includePatterns")
220 .or_else(|| typegen.get("include_patterns"))
221 {
222 if let Ok(patterns) =
223 serde_json::from_value::<Vec<String>>(include_patterns.clone())
224 {
225 config.include_patterns = Some(patterns);
226 }
227 }
228 if let Some(v) = get_bool(&["force"]) {
229 config.force = Some(v);
230 }
231 if let Some(p) = get_string(&["defaultParameterCase", "default_parameter_case"]) {
232 config.default_parameter_case = p;
233 }
234 if let Some(f) = get_string(&["defaultFieldCase", "default_field_case"]) {
235 config.default_field_case = f;
236 }
237
238 return Ok(Some(config));
239 }
240 }
241
242 Ok(None)
243 }
244
245 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
247 let content = serde_json::to_string_pretty(self)?;
248 fs::write(path, content)?;
249 Ok(())
250 }
251
252 pub fn save_to_tauri_config<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
254 if !path.as_ref().exists() {
256 return Err(ConfigError::InvalidConfig(format!(
257 "tauri.conf.json not found at {}. Please ensure you have a Tauri project initialized.",
258 path.as_ref().display()
259 )));
260 }
261
262 let content = fs::read_to_string(&path)?;
263 let mut tauri_config = serde_json::from_str::<serde_json::Value>(&content)?;
264
265 let typegen_config = serde_json::json!({
267 "projectPath": self.project_path,
268 "outputPath": self.output_path,
269 "validationLibrary": self.validation_library,
270 "verbose": self.verbose.unwrap_or(false),
271 "visualizeDeps": self.visualize_deps.unwrap_or(false),
272 "includePrivate": self.include_private.unwrap_or(false),
273 "typeMappings": self.type_mappings,
274 "excludePatterns": self.exclude_patterns,
275 "includePatterns": self.include_patterns,
276 "force": self.force.unwrap_or(false),
277 });
278
279 if !tauri_config.is_object() {
281 tauri_config = serde_json::json!({});
282 }
283
284 let tauri_obj = tauri_config.as_object_mut().unwrap();
285
286 if !tauri_obj.contains_key("plugins") {
288 tauri_obj.insert("plugins".to_string(), serde_json::json!({}));
289 }
290
291 if let Some(plugins) = tauri_obj.get_mut("plugins") {
293 if let Some(plugins_obj) = plugins.as_object_mut() {
294 plugins_obj.insert("typegen".to_string(), typegen_config);
295 }
296 }
297
298 let content = serde_json::to_string_pretty(&tauri_config)?;
299 fs::write(path, content)?;
300 Ok(())
301 }
302
303 pub fn validate(&self) -> Result<(), ConfigError> {
305 match self.validation_library.as_str() {
307 "zod" | "none" => {}
308 _ => {
309 return Err(ConfigError::InvalidValidationLibrary(
310 self.validation_library.clone(),
311 ));
312 }
313 }
314
315 let project_path = Path::new(&self.project_path);
317 if !project_path.exists() {
318 return Err(ConfigError::InvalidConfig(format!(
319 "Project path does not exist: {}",
320 self.project_path
321 )));
322 }
323
324 Ok(())
325 }
326
327 pub fn merge(&mut self, other: &GenerateConfig) {
329 if other.project_path != default_project_path() {
330 self.project_path = other.project_path.clone();
331 }
332 if other.output_path != default_output_path() {
333 self.output_path = other.output_path.clone();
334 }
335 if other.validation_library != default_validation_library() {
336 self.validation_library = other.validation_library.clone();
337 }
338 if other.verbose.is_some() {
339 self.verbose = other.verbose;
340 }
341 if other.visualize_deps.is_some() {
342 self.visualize_deps = other.visualize_deps;
343 }
344 if other.include_private.is_some() {
345 self.include_private = other.include_private;
346 }
347 if other.type_mappings.is_some() {
348 self.type_mappings = other.type_mappings.clone();
349 }
350 if other.exclude_patterns.is_some() {
351 self.exclude_patterns = other.exclude_patterns.clone();
352 }
353 if other.include_patterns.is_some() {
354 self.include_patterns = other.include_patterns.clone();
355 }
356 if other.force.is_some() {
357 self.force = other.force;
358 }
359 }
360
361 pub fn is_verbose(&self) -> bool {
363 self.verbose.unwrap_or(false)
364 }
365
366 pub fn should_visualize_deps(&self) -> bool {
368 self.visualize_deps.unwrap_or(false)
369 }
370
371 pub fn should_include_private(&self) -> bool {
373 self.include_private.unwrap_or(false)
374 }
375
376 pub fn should_force(&self) -> bool {
378 self.force.unwrap_or(false)
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use tempfile::NamedTempFile;
386
387 #[test]
388 fn test_default_config() {
389 let config = GenerateConfig::default();
390 assert_eq!(config.project_path, "./src-tauri");
391 assert_eq!(config.output_path, "./src/generated");
392 assert_eq!(config.validation_library, "none");
393 assert!(!config.is_verbose());
394 assert!(!config.should_visualize_deps());
395 assert!(!config.should_include_private());
396 assert!(!config.should_force());
397 }
398
399 #[test]
400 fn test_config_validation() {
401 let config = GenerateConfig {
402 validation_library: "invalid".to_string(),
403 ..Default::default()
404 };
405
406 let result = config.validate();
407 assert!(result.is_err());
408 if let Err(ConfigError::InvalidValidationLibrary(lib)) = result {
409 assert_eq!(lib, "invalid");
410 } else {
411 panic!("Expected InvalidValidationLibrary error");
412 }
413 }
414
415 #[test]
416 fn test_config_merge() {
417 let mut base = GenerateConfig::default();
418 let override_config = GenerateConfig {
419 output_path: "./custom".to_string(),
420 verbose: Some(true),
421 ..Default::default()
422 };
423
424 base.merge(&override_config);
425 assert_eq!(base.output_path, "./custom");
426 assert!(base.is_verbose());
427 assert_eq!(base.validation_library, "none"); }
429
430 #[test]
431 fn test_save_and_load_config() {
432 let temp_dir = tempfile::TempDir::new().unwrap();
433 let project_path = temp_dir.path().join("src-tauri");
434 std::fs::create_dir_all(&project_path).unwrap();
435
436 let config = GenerateConfig {
437 project_path: project_path.to_string_lossy().to_string(),
438 output_path: "./test".to_string(),
439 verbose: Some(true),
440 ..Default::default()
441 };
442
443 let temp_file = NamedTempFile::new().unwrap();
444 config.save_to_file(temp_file.path()).unwrap();
445
446 let loaded_config = GenerateConfig::from_file(temp_file.path()).unwrap();
447 assert_eq!(loaded_config.output_path, "./test");
448 assert!(loaded_config.is_verbose());
449 }
450
451 #[test]
452 fn test_save_to_tauri_config_preserves_existing_content() {
453 let temp_dir = tempfile::TempDir::new().unwrap();
454 let project_path = temp_dir.path().join("src-tauri");
455 std::fs::create_dir_all(&project_path).unwrap();
456
457 let tauri_conf_path = temp_dir.path().join("tauri.conf.json");
458
459 let existing_content = serde_json::json!({
461 "package": {
462 "productName": "My App",
463 "version": "1.0.0"
464 },
465 "tauri": {
466 "allowlist": {
467 "all": false
468 }
469 },
470 "plugins": {
471 "shell": {
472 "all": false
473 }
474 }
475 });
476
477 fs::write(
478 &tauri_conf_path,
479 serde_json::to_string_pretty(&existing_content).unwrap(),
480 )
481 .unwrap();
482
483 let config = GenerateConfig {
484 project_path: project_path.to_string_lossy().to_string(),
485 output_path: "./test".to_string(),
486 validation_library: "zod".to_string(),
487 verbose: Some(true),
488 ..Default::default()
489 };
490
491 config.save_to_tauri_config(&tauri_conf_path).unwrap();
493
494 let updated_content = fs::read_to_string(&tauri_conf_path).unwrap();
496 let updated_json: serde_json::Value = serde_json::from_str(&updated_content).unwrap();
497
498 assert_eq!(updated_json["package"]["productName"], "My App");
500 assert_eq!(updated_json["package"]["version"], "1.0.0");
501 assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
502 assert_eq!(updated_json["plugins"]["shell"]["all"], false);
503
504 assert_eq!(updated_json["plugins"]["typegen"]["outputPath"], "./test");
506 assert_eq!(
507 updated_json["plugins"]["typegen"]["validationLibrary"],
508 "zod"
509 );
510 assert_eq!(updated_json["plugins"]["typegen"]["verbose"], true);
511 }
512
513 #[test]
514 fn test_save_to_tauri_config_creates_plugins_section() {
515 let temp_dir = tempfile::TempDir::new().unwrap();
516 let project_path = temp_dir.path().join("src-tauri");
517 std::fs::create_dir_all(&project_path).unwrap();
518
519 let tauri_conf_path = temp_dir.path().join("tauri.conf.json");
520
521 let existing_content = serde_json::json!({
523 "package": {
524 "productName": "My App",
525 "version": "1.0.0"
526 },
527 "tauri": {
528 "allowlist": {
529 "all": false
530 }
531 }
532 });
533
534 fs::write(
535 &tauri_conf_path,
536 serde_json::to_string_pretty(&existing_content).unwrap(),
537 )
538 .unwrap();
539
540 let config = GenerateConfig {
541 project_path: project_path.to_string_lossy().to_string(),
542 output_path: "./test".to_string(),
543 validation_library: "none".to_string(),
544 ..Default::default()
545 };
546
547 config.save_to_tauri_config(&tauri_conf_path).unwrap();
549
550 let updated_content = fs::read_to_string(&tauri_conf_path).unwrap();
552 let updated_json: serde_json::Value = serde_json::from_str(&updated_content).unwrap();
553
554 assert_eq!(updated_json["package"]["productName"], "My App");
556 assert_eq!(updated_json["tauri"]["allowlist"]["all"], false);
557
558 assert!(updated_json["plugins"].is_object());
560 assert_eq!(updated_json["plugins"]["typegen"]["outputPath"], "./test");
561 assert_eq!(
562 updated_json["plugins"]["typegen"]["validationLibrary"],
563 "none"
564 );
565 }
566}