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