openapi_nexus_config/
merger.rs

1//! Configuration merger that combines CLI, env, and config file values
2
3use crate::cli::Commands;
4use crate::config::ConfigFile;
5use crate::global_config::GlobalConfig;
6use crate::typescript_config::TypeScriptConfig;
7use openapi_nexus_common::Language;
8
9/// Merge configurations with precedence: CLI > Env > Config File > Defaults
10pub struct ConfigMerger;
11
12impl ConfigMerger {
13    /// Merge CLI arguments with config file, applying precedence rules
14    pub fn merge(
15        config_file: Option<&ConfigFile>,
16        cli_args: &crate::cli::CliArgs,
17    ) -> Result<ResolvedConfig, MergeError> {
18        // Extract CLI configs
19        let cli_global = match &cli_args.command {
20            Commands::Generate { global, .. } => global,
21        };
22
23        let cli_typescript = match &cli_args.command {
24            Commands::Generate { typescript, .. } => typescript,
25        };
26
27        // Extract file configs (if any)
28        let file_typescript = config_file.as_ref().map(|f| &f.typescript);
29
30        // Merge global config
31        // For non-Option fields with defaults, clap has already set them, so we use CLI/env values.
32        // For Option fields, we prefer CLI/env over file.
33        let merged_global = GlobalConfig {
34            input: cli_global.input.clone(),
35            output: cli_global.output.clone(),
36            language: cli_global.language,
37        };
38
39        // Merge TypeScript config
40        // For non-Option fields with defaults, clap has already set them, so we use CLI/env values.
41        // For Option fields, we prefer CLI/env over file.
42        let merged_typescript = TypeScriptConfig {
43            file_naming_convention: cli_typescript.file_naming_convention.clone(),
44            package_scope: cli_typescript
45                .package_scope
46                .clone()
47                .or_else(|| file_typescript.and_then(|c| c.package_scope.clone())),
48            package_name: cli_typescript
49                .package_name
50                .clone()
51                .or_else(|| file_typescript.and_then(|c| c.package_name.clone())),
52            generate_package: cli_typescript.generate_package,
53            ts_target: cli_typescript.ts_target.clone(),
54            ts_module: cli_typescript.ts_module.clone(),
55            ts_lib: cli_typescript
56                .ts_lib
57                .clone()
58                .or_else(|| file_typescript.and_then(|c| c.ts_lib.clone())),
59            generate_esm_config: cli_typescript.generate_esm_config,
60            include_build_scripts: cli_typescript.include_build_scripts,
61        };
62
63        // Resolve and validate
64        let input = merged_global.input.clone();
65        let language = merged_global.language.unwrap_or(Language::TypeScript);
66        let resolved_global = merged_global.resolve(input, language).map_err(|msg| {
67            let err = MergeError::ValidationError(msg.clone());
68            tracing::error!("{}", err);
69            err
70        })?;
71
72        Ok(ResolvedConfig {
73            global: resolved_global,
74            typescript: merged_typescript,
75        })
76    }
77}
78
79/// Resolved configuration with all defaults applied and validations passed
80#[derive(Debug, Clone)]
81pub struct ResolvedConfig {
82    pub global: GlobalConfig,
83    pub typescript: TypeScriptConfig,
84}
85
86/// Error type for configuration merging
87#[derive(Debug)]
88pub enum MergeError {
89    ValidationError(String),
90}
91
92impl std::fmt::Display for MergeError {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        match self {
95            MergeError::ValidationError(msg) => {
96                write!(f, "Configuration validation error: {}", msg)
97            }
98        }
99    }
100}
101
102impl std::error::Error for MergeError {}