1use 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#[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
69pub 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
77pub 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}