1use std::{
4 collections::BTreeSet,
5 fs,
6 path::{Path, PathBuf},
7};
8
9use sim_codec_config::ConfigDecoder;
10use sim_config::{
11 ConfigDir, ConfigLayer, ConfigProbe, ConfigProbeCaps, ConfigProbeReport, ConfigProbeRequest,
12 ConfigRoots, ConfigSecretField, ConfigSource, ConfigTable, EffectiveConfig, ProbeMode,
13 lib_config_path, lib_symbol_from_str, merge_layers,
14};
15use sim_kernel::{Cx, Symbol};
16
17use crate::report::{ConfigSourceReport, SourceStatus};
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ConfigLoadOptions {
22 pub roots: ConfigRoots,
24 pub read_files: bool,
26 pub single_file: Option<PathBuf>,
28 pub site_sources: Vec<Symbol>,
30}
31
32impl ConfigLoadOptions {
33 pub fn with_roots(roots: ConfigRoots) -> Self {
35 Self {
36 roots,
37 read_files: true,
38 single_file: None,
39 site_sources: Vec::new(),
40 }
41 }
42}
43
44impl Default for ConfigLoadOptions {
45 fn default() -> Self {
46 let work_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
47 Self::with_roots(ConfigRoots::from_env(work_root))
48 }
49}
50
51#[derive(Clone, Debug, Default)]
53pub struct RuntimeConfigState {
54 layers: Vec<ConfigLayer>,
55 effective: EffectiveConfig,
56 source_reports: Vec<ConfigSourceReport>,
57 secret_fields: Vec<ConfigSecretField>,
58 probe_reports: Vec<ConfigProbeReport>,
59 diagnostics: Vec<String>,
60}
61
62impl RuntimeConfigState {
63 pub fn layers(&self) -> &[ConfigLayer] {
65 &self.layers
66 }
67
68 pub fn effective(&self) -> &EffectiveConfig {
70 &self.effective
71 }
72
73 pub fn source_reports(&self) -> &[ConfigSourceReport] {
75 &self.source_reports
76 }
77
78 pub fn secret_fields(&self) -> &[ConfigSecretField] {
80 &self.secret_fields
81 }
82
83 pub fn probe_reports(&self) -> &[ConfigProbeReport] {
85 &self.probe_reports
86 }
87
88 pub fn diagnostics(&self) -> &[String] {
90 &self.diagnostics
91 }
92
93 pub fn push_layer(&mut self, layer: ConfigLayer) {
95 self.push_source_report(layer.source.clone(), SourceStatus::Found);
96 self.layers.push(layer);
97 self.effective = merge_layers(&self.layers);
98 }
99
100 pub fn extend_secret_fields(&mut self, fields: impl IntoIterator<Item = ConfigSecretField>) {
102 for field in fields {
103 if !self.secret_fields.contains(&field) {
104 self.secret_fields.push(field);
105 }
106 }
107 }
108
109 pub fn push_probe_report(&mut self, report: ConfigProbeReport) {
111 self.probe_reports.push(report);
112 }
113
114 pub fn push_source_report(&mut self, source: ConfigSource, status: SourceStatus) {
116 self.source_reports
117 .push(ConfigSourceReport { source, status });
118 }
119
120 fn push_diagnostic(&mut self, diagnostic: String) {
121 self.diagnostics.push(diagnostic);
122 }
123}
124
125pub fn load_config_sources(
127 cx: &mut Cx,
128 opts: &ConfigLoadOptions,
129 libs: &[Symbol],
130) -> RuntimeConfigState {
131 load_config_sources_with_probes(cx, opts, libs, &[])
132}
133
134pub fn load_config_sources_with_probes(
136 cx: &mut Cx,
137 opts: &ConfigLoadOptions,
138 libs: &[Symbol],
139 probes: &[&dyn ConfigProbe],
140) -> RuntimeConfigState {
141 let mut state = RuntimeConfigState::default();
142 let libs = unique_libs(libs);
143 run_config_probes(
144 &mut state,
145 &libs,
146 probes,
147 ProbeMode::default(),
148 ConfigProbeCaps::default(),
149 );
150 if opts.read_files {
151 read_root_files(&mut state, &opts.roots.home, RootKind::Home, &libs);
152 read_root_files(
153 &mut state,
154 &Some(opts.roots.work.clone()),
155 RootKind::Work,
156 &libs,
157 );
158 if let Some(path) = opts.single_file.as_ref() {
159 read_single_file(&mut state, path, true);
160 }
161 }
162 for site in &opts.site_sources {
163 read_site_dir(cx, &mut state, site);
164 }
165 state
166}
167
168fn run_config_probes(
169 state: &mut RuntimeConfigState,
170 libs: &[Symbol],
171 probes: &[&dyn ConfigProbe],
172 mode: ProbeMode,
173 caps: ConfigProbeCaps,
174) {
175 for lib in libs {
176 for probe in probes {
177 let request = ConfigProbeRequest {
178 lib: lib.clone(),
179 mode,
180 caps: caps.clone(),
181 };
182 run_config_probe(state, *probe, &request);
183 }
184 }
185}
186
187pub fn run_config_probe(
189 state: &mut RuntimeConfigState,
190 probe: &dyn ConfigProbe,
191 request: &ConfigProbeRequest,
192) {
193 let (layer, report) = probe.probe(request);
194 if let Some(layer) = layer {
195 state.push_layer(layer);
196 }
197 state.push_probe_report(report);
198}
199
200fn unique_libs(libs: &[Symbol]) -> Vec<Symbol> {
201 let mut seen = BTreeSet::new();
202 let mut unique = Vec::new();
203 for lib in libs {
204 if seen.insert(lib.clone()) {
205 unique.push(lib.clone());
206 }
207 }
208 unique
209}
210
211#[derive(Clone, Copy)]
212enum RootKind {
213 Home,
214 Work,
215}
216
217fn read_root_files(
218 state: &mut RuntimeConfigState,
219 root: &Option<PathBuf>,
220 kind: RootKind,
221 libs: &[Symbol],
222) {
223 let Some(root) = root.as_ref() else {
224 return;
225 };
226 for lib in libs {
227 read_per_lib_file(state, root, kind, lib);
228 }
229 read_single_file(state, &root.join("sim.toml"), false);
230}
231
232fn read_per_lib_file(state: &mut RuntimeConfigState, root: &Path, kind: RootKind, lib: &Symbol) {
233 let relative = match lib_config_path(lib) {
234 Ok(relative) => relative,
235 Err(err) => {
236 state.push_diagnostic(format!("skip config path for {lib}: {err}"));
237 return;
238 }
239 };
240 let path = root.join(relative);
241 let source = match kind {
242 RootKind::Home => ConfigSource::HomeFile { path: path.clone() },
243 RootKind::Work => ConfigSource::WorkFile { path: path.clone() },
244 };
245 if !path.exists() {
246 state.push_source_report(source, SourceStatus::Missing);
247 return;
248 }
249 let table = match decode_table_file(&path) {
250 Ok(table) => table,
251 Err(err) => {
252 state.push_source_report(source, SourceStatus::Rejected);
253 state.push_diagnostic(err);
254 return;
255 }
256 };
257 match ConfigDir::one(lib.clone(), table) {
258 Ok(dir) => state.push_layer(ConfigLayer::new(source, dir)),
259 Err(err) => {
260 state.push_source_report(source, SourceStatus::Rejected);
261 state.push_diagnostic(format!("read config {}: {err}", path.display()));
262 }
263 }
264}
265
266fn read_single_file(state: &mut RuntimeConfigState, path: &Path, explicit: bool) {
267 let source = ConfigSource::SingleFile {
268 path: path.to_path_buf(),
269 };
270 if !path.exists() {
271 state.push_source_report(source, SourceStatus::Missing);
272 if explicit {
273 state.push_diagnostic(format!("config file not found: {}", path.display()));
274 }
275 return;
276 }
277 let dir = match decode_dir_file(path) {
278 Ok(dir) => dir,
279 Err(err) => {
280 state.push_source_report(source, SourceStatus::Rejected);
281 state.push_diagnostic(err);
282 return;
283 }
284 };
285 state.push_layer(ConfigLayer::new(source, dir));
286}
287
288fn read_site_dir(cx: &mut Cx, state: &mut RuntimeConfigState, site: &Symbol) {
289 let source = ConfigSource::Site { site: site.clone() };
290 let Some(value) = cx.registry().site_by_symbol(site).cloned() else {
291 state.push_source_report(source, SourceStatus::Missing);
292 state.push_diagnostic(format!("config site not found: {site}"));
293 return;
294 };
295 let expr = match value.object().as_expr(cx) {
296 Ok(expr) => expr,
297 Err(err) => {
298 state.push_source_report(source, SourceStatus::Rejected);
299 state.push_diagnostic(format!("read config site {site}: {err}"));
300 return;
301 }
302 };
303 match ConfigDir::from_dir_expr(&expr).and_then(normalize_dir) {
304 Ok(dir) => state.push_layer(ConfigLayer::new(source, dir)),
305 Err(err) => {
306 state.push_source_report(source, SourceStatus::Rejected);
307 state.push_diagnostic(format!("read config site {site}: {err}"));
308 }
309 }
310}
311
312fn decode_table_file(path: &Path) -> Result<sim_kernel::Expr, String> {
313 let source = read_ascii(path)?;
314 ConfigDecoder::table()
315 .decode_text(&source)
316 .map_err(|err| format!("decode config table {}: {err}", path.display()))
317}
318
319fn decode_dir_file(path: &Path) -> Result<ConfigDir, String> {
320 let source = read_ascii(path)?;
321 let expr = ConfigDecoder::dir()
322 .decode_text(&source)
323 .map_err(|err| format!("decode config dir {}: {err}", path.display()))?;
324 ConfigDir::from_dir_expr(&expr)
325 .and_then(normalize_dir)
326 .map_err(|err| format!("decode config dir {}: {err}", path.display()))
327}
328
329fn normalize_dir(dir: ConfigDir) -> sim_config::ConfigResult<ConfigDir> {
330 let mut normalized = ConfigDir::new();
331 for table in dir.entries {
332 let lib = lib_symbol_from_str(&table.lib.as_qualified_str())?;
333 normalized.upsert(ConfigTable::new(lib, table.table)?);
334 }
335 Ok(normalized)
336}
337
338fn read_ascii(path: &Path) -> Result<String, String> {
339 fs::read_to_string(path).map_err(|err| format!("read config {}: {err}", path.display()))
340}