Skip to main content

actr_config/user_config/
resolver.rs

1//! Shared user configuration resolver.
2
3use super::loader::{global_config_path, load_cli_config, local_config_path};
4use super::schema::{
5    CacheConfig, CliConfig, CodegenConfig, InstallConfig, MfrConfig, NetworkConfig, StorageConfig,
6    UiConfig,
7};
8use crate::error::Result;
9use std::path::PathBuf;
10
11/// Fully-resolved user config with all defaults applied.
12#[derive(Debug, Clone)]
13pub struct EffectiveCliConfig {
14    pub mfr: EffectiveMfrConfig,
15    pub codegen: EffectiveCodegenConfig,
16    pub cache: EffectiveCacheConfig,
17    pub ui: EffectiveUiConfig,
18    pub network: EffectiveNetworkConfig,
19    pub storage: EffectiveStorageConfig,
20}
21
22#[derive(Debug, Clone)]
23pub struct EffectiveMfrConfig {
24    pub manufacturer: String,
25    pub keychain: Option<String>,
26}
27
28#[derive(Debug, Clone)]
29pub struct EffectiveCodegenConfig {
30    pub language: String,
31    pub output: String,
32    pub clean_before_generate: bool,
33}
34
35#[derive(Debug, Clone)]
36pub struct EffectiveCacheConfig {
37    pub dir: String,
38    pub auto_lock: bool,
39    pub prefer_cache: bool,
40}
41
42#[derive(Debug, Clone)]
43pub struct EffectiveUiConfig {
44    pub format: String,
45    pub verbose: bool,
46    pub color: String,
47    pub non_interactive: bool,
48}
49
50#[derive(Debug, Clone)]
51pub struct EffectiveNetworkConfig {
52    pub signaling_url: String,
53    pub ais_endpoint: String,
54    pub realm_id: Option<u32>,
55    pub realm_secret: Option<String>,
56}
57
58#[derive(Debug, Clone)]
59pub struct EffectiveStorageConfig {
60    pub hyper_data_dir: PathBuf,
61}
62
63impl Default for EffectiveCliConfig {
64    fn default() -> Self {
65        apply_defaults(CliConfig::default())
66    }
67}
68
69/// Resolve the effective CLI/user config by merging global and local configs, then applying defaults.
70pub fn resolve_effective_cli_config() -> Result<EffectiveCliConfig> {
71    let global = load_cli_config(&global_config_path()?)?;
72    let local = load_cli_config(&local_config_path())?;
73    let merged = merge_configs(global, local);
74    Ok(apply_defaults(merged))
75}
76
77/// Resolve only the effective Hyper data directory from the shared user config.
78pub fn resolve_hyper_data_dir() -> Result<PathBuf> {
79    Ok(resolve_effective_cli_config()?.storage.hyper_data_dir)
80}
81
82fn merge_configs(base: Option<CliConfig>, overlay: Option<CliConfig>) -> CliConfig {
83    match (base, overlay) {
84        (None, None) => CliConfig::default(),
85        (Some(b), None) => b,
86        (None, Some(o)) => o,
87        (Some(b), Some(o)) => CliConfig {
88            version: o.version.or(b.version),
89            mfr: MfrConfig {
90                manufacturer: o.mfr.manufacturer.or(b.mfr.manufacturer),
91                keychain: o.mfr.keychain.or(b.mfr.keychain),
92            },
93            codegen: CodegenConfig {
94                language: o.codegen.language.or(b.codegen.language),
95                output: o.codegen.output.or(b.codegen.output),
96                clean_before_generate: o
97                    .codegen
98                    .clean_before_generate
99                    .or(b.codegen.clean_before_generate),
100            },
101            cache: CacheConfig {
102                dir: o.cache.dir.or(b.cache.dir),
103                auto_lock: o.cache.auto_lock.or(b.cache.auto_lock),
104                prefer_cache: o.cache.prefer_cache.or(b.cache.prefer_cache),
105            },
106            ui: UiConfig {
107                format: o.ui.format.or(b.ui.format),
108                verbose: o.ui.verbose.or(b.ui.verbose),
109                color: o.ui.color.or(b.ui.color),
110                non_interactive: o.ui.non_interactive.or(b.ui.non_interactive),
111            },
112            network: NetworkConfig {
113                signaling_url: o.network.signaling_url.or(b.network.signaling_url),
114                ais_endpoint: o.network.ais_endpoint.or(b.network.ais_endpoint),
115                realm_id: o.network.realm_id.or(b.network.realm_id),
116                realm_secret: o.network.realm_secret.or(b.network.realm_secret),
117            },
118            install: InstallConfig {},
119            storage: StorageConfig {
120                hyper_data_dir: o.storage.hyper_data_dir.or(b.storage.hyper_data_dir),
121            },
122        },
123    }
124}
125
126fn apply_defaults(cfg: CliConfig) -> EffectiveCliConfig {
127    EffectiveCliConfig {
128        mfr: EffectiveMfrConfig {
129            manufacturer: cfg.mfr.manufacturer.unwrap_or_else(|| "acme".to_string()),
130            keychain: cfg
131                .mfr
132                .keychain
133                .map(|path| expand_tilde(path).to_string_lossy().to_string()),
134        },
135        codegen: EffectiveCodegenConfig {
136            language: cfg.codegen.language.unwrap_or_else(|| "rust".to_string()),
137            output: cfg
138                .codegen
139                .output
140                .unwrap_or_else(|| "src/generated".to_string()),
141            clean_before_generate: cfg.codegen.clean_before_generate.unwrap_or(false),
142        },
143        cache: EffectiveCacheConfig {
144            dir: cfg.cache.dir.unwrap_or_else(|| "~/.actr/cache".to_string()),
145            auto_lock: cfg.cache.auto_lock.unwrap_or(true),
146            prefer_cache: cfg.cache.prefer_cache.unwrap_or(true),
147        },
148        ui: EffectiveUiConfig {
149            format: cfg.ui.format.unwrap_or_else(|| "toml".to_string()),
150            verbose: cfg.ui.verbose.unwrap_or(false),
151            color: cfg.ui.color.unwrap_or_else(|| "auto".to_string()),
152            non_interactive: cfg.ui.non_interactive.unwrap_or(false),
153        },
154        network: EffectiveNetworkConfig {
155            signaling_url: cfg
156                .network
157                .signaling_url
158                .unwrap_or_else(|| "ws://localhost:8081/signaling/ws".to_string()),
159            ais_endpoint: cfg
160                .network
161                .ais_endpoint
162                .unwrap_or_else(|| "http://localhost:8081/ais".to_string()),
163            realm_id: cfg.network.realm_id.or(Some(1)),
164            realm_secret: cfg.network.realm_secret,
165        },
166        storage: EffectiveStorageConfig {
167            hyper_data_dir: cfg
168                .storage
169                .hyper_data_dir
170                .map(expand_tilde)
171                .unwrap_or_else(|| expand_tilde("~/.actr/hyper".to_string())),
172        },
173    }
174}
175
176fn expand_tilde(path: String) -> PathBuf {
177    if let Some(stripped) = path.strip_prefix("~/") {
178        if let Some(home) = dirs::home_dir() {
179            return home.join(stripped);
180        }
181    }
182    PathBuf::from(path)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn defaults_include_global_hyper_dir() {
191        let effective = EffectiveCliConfig::default();
192        assert_eq!(
193            effective.storage.hyper_data_dir,
194            expand_tilde("~/.actr/hyper".to_string())
195        );
196    }
197
198    #[test]
199    fn overlay_wins_for_storage() {
200        let base = CliConfig {
201            storage: StorageConfig {
202                hyper_data_dir: Some("/tmp/base".to_string()),
203            },
204            ..Default::default()
205        };
206        let overlay = CliConfig {
207            storage: StorageConfig {
208                hyper_data_dir: Some("/tmp/overlay".to_string()),
209            },
210            ..Default::default()
211        };
212
213        let merged = merge_configs(Some(base), Some(overlay));
214        assert_eq!(
215            merged.storage.hyper_data_dir.as_deref(),
216            Some("/tmp/overlay")
217        );
218    }
219}