Skip to main content

oas_forge/
config.rs

1#[cfg(feature = "cli")]
2use clap::Parser;
3use serde::Deserialize;
4use std::path::PathBuf;
5
6#[derive(Debug, Deserialize, Default, Clone)]
7#[cfg_attr(feature = "cli", derive(Parser))]
8#[serde(default)]
9#[cfg_attr(feature = "cli", command(author, version, about, long_about = None))]
10pub struct Config {
11    /// Input directories to scan for Rust files and OpenAPI fragments
12    #[cfg_attr(feature = "cli", arg(short = 'i', long = "input"))]
13    pub input: Option<Vec<PathBuf>>,
14
15    /// Specific files to include (e.g., .json, .yaml)
16    #[cfg_attr(feature = "cli", arg(long = "include"))]
17    pub include: Option<Vec<PathBuf>>,
18
19    /// Output file(s) for the generated OpenAPI definition (defaults to openapi.yaml)
20    #[cfg_attr(feature = "cli", arg(short = 'o', long = "output", alias = "out"))]
21    pub output: Option<Vec<PathBuf>>,
22
23    /// Output file(s) for just the components/schemas section
24    #[cfg_attr(feature = "cli", arg(long = "output-schemas"))]
25    pub output_schemas: Option<Vec<PathBuf>>,
26
27    /// Output file(s) for just the paths section
28    #[cfg_attr(feature = "cli", arg(long = "output-paths"))]
29    pub output_paths: Option<Vec<PathBuf>>,
30
31    /// Output file(s) for the full spec minus root details (fragments)
32    #[cfg_attr(feature = "cli", arg(long = "output-fragments"))]
33    pub output_fragments: Option<Vec<PathBuf>>,
34
35    /// Path to a configuration file (toml)
36    #[cfg_attr(feature = "cli", arg(long = "config"))]
37    #[serde(skip)]
38    pub config_file: Option<PathBuf>,
39}
40
41#[derive(Deserialize)]
42struct CargoConfig {
43    package: Option<CargoPackage>,
44}
45
46#[derive(Deserialize)]
47struct CargoPackage {
48    metadata: Option<CargoMetadata>,
49}
50
51#[derive(Deserialize)]
52struct CargoMetadata {
53    #[serde(rename = "oas-forge")]
54    oas_forge: Option<Config>,
55}
56
57impl Config {
58    /// Load configuration with priority (CLI mode only):
59    /// 1. CLI Arguments (Highest)
60    /// 2. --config file
61    /// 3. openapi.toml
62    /// 4. Cargo.toml [package.metadata.oas-forge]
63    #[cfg(feature = "cli")]
64    pub fn load() -> Self {
65        let cli_args = Config::parse();
66
67        // Start with default empty config
68        let mut final_config = Config::default();
69
70        // 4. Try loading Cargo.toml
71        if let Ok(cargo_conf) = load_cargo_toml() {
72            final_config.merge(cargo_conf);
73        }
74
75        // 3. Try loading openapi.toml
76        if let Ok(toml_conf) = load_toml_file("openapi.toml") {
77            final_config.merge(toml_conf);
78        }
79
80        // 2. Try loading explicit config file
81        if let Some(path) = &cli_args.config_file {
82            if let Ok(file_conf) = load_toml_file(path) {
83                final_config.merge(file_conf);
84            }
85        }
86
87        // 1. Merge CLI args (taking precedence)
88        final_config.merge(cli_args);
89
90        final_config
91    }
92
93    fn merge(&mut self, other: Config) {
94        if let Some(input) = other.input {
95            self.input = Some(input);
96        }
97        if let Some(include) = other.include {
98            self.include = Some(include);
99        }
100        if let Some(output) = other.output {
101            self.output = Some(output);
102        }
103        if let Some(output_schemas) = other.output_schemas {
104            self.output_schemas = Some(output_schemas);
105        }
106        if let Some(output_paths) = other.output_paths {
107            self.output_paths = Some(output_paths);
108        }
109        if let Some(output_fragments) = other.output_fragments {
110            self.output_fragments = Some(output_fragments);
111        }
112    }
113}
114
115fn load_cargo_toml() -> Result<Config, Box<dyn std::error::Error>> {
116    let content = std::fs::read_to_string("Cargo.toml")?;
117    let config: CargoConfig = toml::from_str(&content)?;
118    Ok(config
119        .package
120        .and_then(|p| p.metadata)
121        .and_then(|m| m.oas_forge)
122        .unwrap_or_default())
123}
124
125fn load_toml_file<P: AsRef<std::path::Path>>(
126    path: P,
127) -> Result<Config, Box<dyn std::error::Error>> {
128    let content = std::fs::read_to_string(path)?;
129    let config: Config = toml::from_str(&content)?;
130    Ok(config)
131}