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