oas_forge/
config.rs

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