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