sim_config/lib.rs
1//! Table and Dir substrate for layered SIM configuration.
2//!
3//! Configuration is plain kernel data: a [`ConfigTable`] is one library's
4//! `Expr::Map`, and a [`ConfigDir`] maps library ids to those tables. This crate
5//! provides the data model, merge/provenance rules, shape-backed defaults,
6//! typed read views, and safe path helpers used by loaders and codecs without
7//! introducing a parallel configuration value enum.
8//!
9//! # Example
10//!
11//! ```
12//! use sim_config::{ConfigDir, ConfigLayer, ConfigSource, merge_layers};
13//! use sim_kernel::{Expr, Symbol};
14//! use sim_value::build::{entry, map, text};
15//!
16//! let lib = Symbol::qualified("sim", "cookbook");
17//! let lower = ConfigDir::one(lib.clone(), map(vec![("mode", text("built-in"))])).unwrap();
18//! let upper = ConfigDir::one(lib.clone(), map(vec![("mode", text("work"))])).unwrap();
19//! let effective = merge_layers(&[
20//! ConfigLayer::new(ConfigSource::BuiltIn { lib: lib.clone() }, lower),
21//! ConfigLayer::new(ConfigSource::Explicit { label: "work".into() }, upper),
22//! ]);
23//!
24//! let table = effective.dir.table(&lib).unwrap();
25//! assert_eq!(sim_value::access::field(&table.table, "mode"), Some(&Expr::String("work".into())));
26//! assert_eq!(effective.trace[0].key, "mode");
27//! let _ = entry("ok", Expr::Bool(true));
28//! ```
29
30#![forbid(unsafe_code)]
31#![deny(missing_docs)]
32
33use std::fmt;
34
35use sim_kernel::{Expr, Symbol, id::SymbolError};
36
37pub mod merge;
38pub mod path;
39pub mod probe;
40pub mod report;
41pub mod shape;
42pub mod source;
43pub mod table;
44pub mod view;
45
46pub use merge::{ConfigLayer, EffectiveConfig, MergeTrace, merge_layers};
47pub use path::{ConfigRoots, lib_config_path};
48pub use probe::{
49 ConfigProbe, ConfigProbeCaps, ConfigProbeReport, ConfigProbeRequest, ConfigProbeStatus,
50};
51pub use report::{ConfigReport, ConfigReportEntry};
52pub use shape::{ConfigSecretField, ConfigShape, ConfigShapeResult, LibConfigDefaults};
53pub use source::{ConfigSource, ProbeMode};
54pub use table::{ConfigDir, ConfigTable, lib_symbol_from_str};
55pub use view::ConfigView;
56
57/// Result type used by `sim-config` helpers.
58pub type ConfigResult<T> = Result<T, ConfigError>;
59
60/// Error reported by config table, view, and path helpers.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub enum ConfigError {
63 /// A Dir expression was not an `Expr::Map`.
64 NonDirExpr,
65 /// A table expression was not an `Expr::Map`.
66 NonTableExpr,
67 /// A Dir entry for `lib` did not contain a table value.
68 NonTableConfig {
69 /// The library whose Dir entry was malformed.
70 lib: Symbol,
71 },
72 /// A Dir key could not be interpreted as a library id.
73 UnsupportedDirKey {
74 /// The unsupported key expression.
75 key: Expr,
76 },
77 /// A library id or id segment is not safe for config use.
78 InvalidLibId {
79 /// The rejected id.
80 id: String,
81 },
82 /// A library config path segment was rejected.
83 InvalidPathSegment {
84 /// The rejected segment.
85 segment: String,
86 },
87 /// A required field was absent from a config table.
88 MissingField {
89 /// The missing field name.
90 key: String,
91 },
92 /// A config field had a different shape than the caller requested.
93 TypeMismatch {
94 /// The field name.
95 key: String,
96 /// The expected shape.
97 expected: &'static str,
98 },
99}
100
101impl From<SymbolError> for ConfigError {
102 fn from(error: SymbolError) -> Self {
103 Self::InvalidLibId {
104 id: error.to_string(),
105 }
106 }
107}
108
109impl fmt::Display for ConfigError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Self::NonDirExpr => f.write_str("config dir must be an Expr::Map"),
113 Self::NonTableExpr => f.write_str("config table must be an Expr::Map"),
114 Self::NonTableConfig { lib } => {
115 write!(f, "config for `{}` must be a table", lib.as_qualified_str())
116 }
117 Self::UnsupportedDirKey { key } => {
118 write!(f, "config dir key {key:?} is not a supported library id")
119 }
120 Self::InvalidLibId { id } => write!(f, "invalid config library id `{id}`"),
121 Self::InvalidPathSegment { segment } => {
122 write!(f, "invalid config path segment `{segment}`")
123 }
124 Self::MissingField { key } => write!(f, "config field `{key}` is missing"),
125 Self::TypeMismatch { key, expected } => {
126 write!(f, "config field `{key}` is not {expected}")
127 }
128 }
129 }
130}
131
132impl std::error::Error for ConfigError {}