1use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39use std::path::{Path, PathBuf};
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "kebab-case")]
44pub struct GenerateConfig {
45 pub input: InputConfig,
47 pub output: OutputConfig,
49 #[serde(default)]
51 pub plugins: HashMap<String, PluginConfig>,
52 pub options: Option<GenerateOptions>,
54}
55
56impl Default for GenerateConfig {
57 fn default() -> Self {
58 Self {
59 input: InputConfig::default(),
60 output: OutputConfig::default(),
61 plugins: HashMap::new(),
62 options: Some(GenerateOptions::default()),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case")]
70#[derive(Default)]
71pub struct InputConfig {
72 pub spec: Option<PathBuf>,
74 #[serde(default)]
76 pub additional: Vec<PathBuf>,
77}
78
79#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
81#[serde(rename_all = "kebab-case")]
82pub enum BarrelType {
83 None,
85 #[serde(rename = "index")]
87 Index,
88 #[serde(rename = "barrel")]
90 Barrel,
91}
92
93impl Default for BarrelType {
94 fn default() -> Self {
95 Self::None
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "kebab-case")]
102pub struct OutputConfig {
103 pub path: PathBuf,
105 pub filename: Option<String>,
107 #[serde(default)]
109 pub clean: bool,
110 #[serde(default)]
112 pub barrel_type: BarrelType,
113 pub extension: Option<String>,
115 pub banner: Option<String>,
118 pub file_naming_template: Option<String>,
121}
122
123impl Default for OutputConfig {
124 fn default() -> Self {
125 Self {
126 path: PathBuf::from("./generated"),
127 filename: None,
128 clean: false,
129 barrel_type: BarrelType::None,
130 extension: None,
131 banner: None,
132 file_naming_template: None,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum PluginConfig {
141 Simple(String),
143 Advanced {
145 package: String,
147 #[serde(default)]
149 options: HashMap<String, serde_json::Value>,
150 },
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "kebab-case")]
156pub struct GenerateOptions {
157 pub client: Option<String>,
159 pub mode: Option<String>,
161 pub include_validation: bool,
163 pub include_examples: bool,
165 pub runtime: Option<String>,
167}
168
169impl Default for GenerateOptions {
170 fn default() -> Self {
171 Self {
172 client: Some("reqwest".to_string()),
173 mode: Some("tags".to_string()),
174 include_validation: true,
175 include_examples: true,
176 runtime: Some("tokio".to_string()),
177 }
178 }
179}
180
181pub fn discover_config_file() -> Result<PathBuf, String> {
183 let config_names = vec![
184 "mockforge.toml",
185 "mockforge.json",
186 "mockforge.yaml",
187 "mockforge.yml",
188 ".mockforge.toml",
189 ".mockforge.json",
190 ".mockforge.yaml",
191 ".mockforge.yml",
192 ];
193
194 for name in config_names {
195 let path = Path::new(&name);
196 if path.exists() {
197 return Ok(path.to_path_buf());
198 }
199 }
200
201 Err("No configuration file found".to_string())
202}
203
204pub async fn load_generate_config<P: AsRef<Path>>(path: P) -> crate::Result<GenerateConfig> {
206 let path = path.as_ref();
207
208 if !path.exists() {
209 return Ok(GenerateConfig::default());
210 }
211
212 let content = tokio::fs::read_to_string(path)
213 .await
214 .map_err(|e| crate::Error::generic(format!("Failed to read config file: {}", e)))?;
215
216 let config = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
217 toml::from_str(&content)
218 .map_err(|e| crate::Error::generic(format!("Failed to parse TOML config: {}", e)))?
219 } else if path.extension().and_then(|s| s.to_str()).map(|s| s == "json").unwrap_or(false) {
220 serde_json::from_str(&content)
221 .map_err(|e| crate::Error::generic(format!("Failed to parse JSON config: {}", e)))?
222 } else {
223 serde_yaml::from_str(&content)
225 .map_err(|e| crate::Error::generic(format!("Failed to parse YAML config: {}", e)))?
226 };
227
228 Ok(config)
229}
230
231pub async fn load_generate_config_with_fallback<P: AsRef<Path>>(path: P) -> GenerateConfig {
233 match load_generate_config(path).await {
234 Ok(config) => config,
235 Err(e) => {
236 eprintln!("Warning: Failed to load config file: {}. Using defaults.", e);
237 GenerateConfig::default()
238 }
239 }
240}
241
242pub async fn save_generate_config<P: AsRef<Path>>(
244 path: P,
245 config: &GenerateConfig,
246) -> crate::Result<()> {
247 let path = path.as_ref();
248
249 let content = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
250 toml::to_string_pretty(config)
251 .map_err(|e| crate::Error::generic(format!("Failed to serialize to TOML: {}", e)))?
252 } else if path.extension().and_then(|s| s.to_str()).map(|s| s == "json").unwrap_or(false) {
253 serde_json::to_string_pretty(config)
254 .map_err(|e| crate::Error::generic(format!("Failed to serialize to JSON: {}", e)))?
255 } else {
256 serde_yaml::to_string(config)
257 .map_err(|e| crate::Error::generic(format!("Failed to serialize to YAML: {}", e)))?
258 };
259
260 tokio::fs::write(path, content)
261 .await
262 .map_err(|e| crate::Error::generic(format!("Failed to write config file: {}", e)))?;
263
264 Ok(())
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_default_config() {
273 let config = GenerateConfig::default();
274 assert!(config.input.spec.is_none());
275 assert_eq!(config.output.path, PathBuf::from("./generated"));
276 assert!(config.plugins.is_empty());
277 assert!(config.options.is_some());
278 }
279
280 #[test]
281 fn test_config_serialization_toml() {
282 let config = GenerateConfig::default();
283 let toml = toml::to_string(&config).unwrap();
284 assert!(toml.contains("input"));
285 assert!(toml.contains("output"));
286 }
287
288 #[test]
289 fn test_config_serialization_json() {
290 let config = GenerateConfig::default();
291 let json = serde_json::to_string(&config).unwrap();
292 assert!(json.contains("input"));
293 assert!(json.contains("output"));
294 }
295
296 #[test]
297 fn test_config_deserialization_toml() {
298 let toml_str = r#"
299[input]
300spec = "openapi.json"
301
302[output]
303path = "./generated"
304clean = true
305"#;
306 let config: GenerateConfig = toml::from_str(toml_str).unwrap();
307 assert_eq!(config.input.spec.unwrap(), PathBuf::from("openapi.json"));
308 assert_eq!(config.output.path, PathBuf::from("./generated"));
309 assert!(config.output.clean);
310 }
311
312 #[test]
313 fn test_output_config_with_barrel_type() {
314 let toml_str = r#"
315[output]
316path = "./generated"
317barrel-type = "index"
318extension = "ts"
319banner = "Generated by {{generator}}"
320"#;
321 let config: GenerateConfig = toml::from_str(toml_str).unwrap();
322 assert_eq!(config.output.barrel_type, BarrelType::Index);
323 assert_eq!(config.output.extension, Some("ts".to_string()));
324 assert!(config.output.banner.is_some());
325 }
326
327 #[test]
328 fn test_config_deserialization_json() {
329 let json_str = r#"{
330 "input": {
331 "spec": "openapi.json"
332 },
333 "output": {
334 "path": "./generated",
335 "clean": true
336 },
337 "options": {
338 "client": "reqwest",
339 "mode": "tags",
340 "include-validation": true,
341 "include-examples": true
342 }
343 }"#;
344 let config: GenerateConfig = serde_json::from_str(json_str).unwrap();
345 assert_eq!(config.input.spec.unwrap(), PathBuf::from("openapi.json"));
346 assert_eq!(config.output.path, PathBuf::from("./generated"));
347 assert!(config.output.clean);
348 assert!(config.options.is_some());
349 }
350
351 #[test]
352 fn test_plugin_config_simple() {
353 let json_str = r#"{
354 "plugin-name": "package-name"
355 }"#;
356 let plugins: HashMap<String, PluginConfig> = serde_json::from_str(json_str).unwrap();
357 match plugins.get("plugin-name").unwrap() {
358 PluginConfig::Simple(pkg) => assert_eq!(pkg, "package-name"),
359 _ => panic!("Expected simple plugin"),
360 }
361 }
362
363 #[test]
364 fn test_plugin_config_advanced() {
365 let json_str = r#"{
366 "plugin-name": {
367 "package": "package-name",
368 "options": {
369 "key": "value"
370 }
371 }
372 }"#;
373 let plugins: HashMap<String, PluginConfig> = serde_json::from_str(json_str).unwrap();
374 match plugins.get("plugin-name").unwrap() {
375 PluginConfig::Advanced { package, options } => {
376 assert_eq!(package, "package-name");
377 assert!(options.contains_key("key"));
378 }
379 _ => panic!("Expected advanced plugin"),
380 }
381 }
382}