Skip to main content

commit_wizard/engine/config/
resolver.rs

1use crate::engine::{
2    LoggerTrait,
3    config::{BaseConfig, ProjectConfig, RulesConfig, StandardConfig},
4    constants::{resolve_global_config_path, resolve_global_rules_path},
5    error::{ErrorCode, Result},
6};
7
8pub fn resolve_global_configs() -> (Option<BaseConfig>, Option<RulesConfig>) {
9    // if global config path exists, load it
10    let global_config = if let Ok(path) = resolve_global_config_path() {
11        resolve_standard_configs(&path)
12    } else {
13        None
14    };
15
16    // if global rules path exists, load it
17    let global_rules = if let Ok(path) = resolve_global_rules_path() {
18        load_rules_config(&path)
19    } else {
20        None
21    };
22    (global_config, global_rules)
23}
24
25pub fn resolve_project_configs(
26    path: &std::path::Path,
27    logger: Option<&dyn LoggerTrait>,
28) -> (Option<BaseConfig>, Option<RulesConfig>) {
29    if let Some(project_config) = load_project_config(path, logger) {
30        let (base_config, rules_config) = extract_configs_from_project_config(&project_config);
31        (Some(base_config), rules_config)
32    } else {
33        (None, None)
34    }
35}
36
37pub fn resolve_standard_configs(path: &std::path::Path) -> Option<BaseConfig> {
38    if let Some(standard_config) = load_standard_config(path) {
39        let base_config = extract_config_from_standard_config(&standard_config);
40        Some(base_config)
41    } else {
42        None
43    }
44}
45
46pub fn load_project_config(
47    path: &std::path::Path,
48    logger: Option<&dyn LoggerTrait>,
49) -> Option<ProjectConfig> {
50    if !path.exists() {
51        if let Some(logger) = logger {
52            let msg = format!("load_project_config: path does not exist: {:?}", path);
53            logger.debug(&msg);
54        }
55        return None;
56    }
57
58    match std::fs::read_to_string(path) {
59        Ok(content) => {
60            if let Some(logger) = logger {
61                let msg = format!(
62                    "load_project_config: Read {} bytes from {:?}",
63                    content.len(),
64                    path
65                );
66                logger.debug(&msg);
67            }
68            match ProjectConfig::from_toml_str(&content) {
69                Ok(config) => {
70                    if let Some(logger) = logger {
71                        logger.debug("load_project_config: Successfully parsed config");
72                    }
73                    Some(config)
74                }
75                Err(e) => {
76                    if let Some(logger) = logger {
77                        // Include both the main error message and any context details
78                        let msg = format!("load_project_config: Parse error: {}", e);
79                        logger.error(&msg);
80                        // Also log context information if available
81                        for (key, value) in &e.context {
82                            let ctx_msg = format!("    {}: {}", key, value);
83                            logger.error(&ctx_msg);
84                        }
85                    }
86                    None
87                }
88            }
89        }
90        Err(e) => {
91            if let Some(logger) = logger {
92                let msg = format!("load_project_config: File read error: {}", e);
93                logger.error(&msg);
94            }
95            None
96        }
97    }
98}
99
100pub fn load_standard_config(path: &std::path::Path) -> Option<StandardConfig> {
101    if !path.exists() {
102        return None;
103    }
104
105    match std::fs::read_to_string(path) {
106        Ok(content) => StandardConfig::from_toml_str(&content).ok(),
107        Err(_) => None,
108    }
109}
110
111pub fn load_rules_config(path: &std::path::Path) -> Option<RulesConfig> {
112    if !path.exists() {
113        return None;
114    }
115
116    match std::fs::read_to_string(path) {
117        Ok(content) => RulesConfig::from_toml_str(&content).ok(),
118        Err(_) => None,
119    }
120}
121
122pub fn extract_configs_from_project_config(
123    project_config: &ProjectConfig,
124) -> (BaseConfig, Option<RulesConfig>) {
125    (
126        project_config.inner.config.clone(),
127        project_config.inner.rules.clone(),
128    )
129}
130
131pub fn extract_config_from_standard_config(standard_config: &StandardConfig) -> BaseConfig {
132    standard_config.inner.clone()
133}
134
135/// Merge rules into base config by resolving all `@rules.*` references.
136///
137/// Strategy: serialize `BaseConfig` to a `toml::Value` tree, recursively resolve
138/// every string that is a `@rules.*` reference using the existing `resolve_value_refs`
139/// walker, then deserialize back to `BaseConfig`.
140///
141/// Per SRS §4.3: resolution is evaluated after rules merging, before validation.
142/// Failure to resolve MUST error.
143pub fn merge_rules_into_base(base: BaseConfig, rules: &RulesConfig) -> Result<BaseConfig> {
144    // Serialize to an intermediate toml::Value so we can walk the whole tree
145    // generically without enumerating every field individually.
146    let mut value = toml::Value::try_from(&base).map_err(|e| {
147        ErrorCode::SerializationFailure
148            .error()
149            .with_context("operation", "merge_rules_into_base: serialize")
150            .with_context("error", e.to_string())
151    })?;
152
153    // Walk the entire tree and resolve any @rules.* strings in-place.
154    // This covers every Option<String>, Vec<String>, and nested struct field.
155    rules.resolve_value_refs(&mut value)?;
156
157    // Deserialize back into BaseConfig — if a resolved value is the wrong type,
158    // this will produce a descriptive error before validation runs.
159    toml::Value::try_into(value).map_err(|e| {
160        ErrorCode::SerializationFailure
161            .error()
162            .with_context("operation", "merge_rules_into_base: deserialize")
163            .with_context("error", e.to_string())
164    })
165}