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