1use sim_kernel::Expr;
4use sim_value::access::{as_i64, as_str};
5
6use crate::{ConfigError, ConfigResult, ConfigTable, config_field_name};
7
8#[derive(Clone, Copy, Debug)]
10pub struct ConfigView<'a> {
11 entries: &'a [(Expr, Expr)],
12}
13
14impl<'a> ConfigView<'a> {
15 pub fn new(table: &'a ConfigTable) -> Self {
17 let Expr::Map(entries) = &table.table else {
18 unreachable!("ConfigTable guarantees an Expr::Map table")
19 };
20 Self { entries }
21 }
22
23 pub fn from_entries(entries: &'a [(Expr, Expr)]) -> Self {
25 Self { entries }
26 }
27
28 pub fn get(&self, key: &str) -> Option<&'a Expr> {
30 self.entries.iter().find_map(|(field_key, value)| {
31 config_field_name(field_key)
32 .is_some_and(|field| field == key)
33 .then_some(value)
34 })
35 }
36
37 pub fn string(&self, key: &str) -> Option<&'a str> {
39 self.get(key).and_then(as_str)
40 }
41
42 pub fn required_string(&self, key: &str) -> ConfigResult<&'a str> {
44 self.string(key)
45 .ok_or_else(|| self.required_error(key, "a string"))
46 }
47
48 pub fn bool(&self, key: &str) -> Option<bool> {
50 match self.get(key) {
51 Some(Expr::Bool(value)) => Some(*value),
52 _ => None,
53 }
54 }
55
56 pub fn required_bool(&self, key: &str) -> ConfigResult<bool> {
58 self.bool(key)
59 .ok_or_else(|| self.required_error(key, "a bool"))
60 }
61
62 pub fn i64(&self, key: &str) -> Option<i64> {
64 self.get(key).and_then(as_i64)
65 }
66
67 pub fn required_i64(&self, key: &str) -> ConfigResult<i64> {
69 self.i64(key)
70 .ok_or_else(|| self.required_error(key, "an integer"))
71 }
72
73 pub fn list(&self, key: &str) -> Option<&'a [Expr]> {
75 match self.get(key) {
76 Some(Expr::List(items)) => Some(items),
77 _ => None,
78 }
79 }
80
81 pub fn table(&self, key: &str) -> Option<&'a [(Expr, Expr)]> {
83 match self.get(key) {
84 Some(Expr::Map(entries)) => Some(entries),
85 _ => None,
86 }
87 }
88
89 pub fn string_array(&self, key: &str) -> ConfigResult<Vec<String>> {
95 let Some(value) = self.get(key) else {
96 return Ok(Vec::new());
97 };
98 let Expr::List(items) = value else {
99 return Err(ConfigError::TypeMismatch {
100 key: key.to_owned(),
101 expected: "a string list",
102 });
103 };
104 items
105 .iter()
106 .enumerate()
107 .map(|(index, item)| match item {
108 Expr::String(value) => Ok(value.clone()),
109 _ => Err(ConfigError::TypeMismatch {
110 key: format!("{key}[{index}]"),
111 expected: "a string",
112 }),
113 })
114 .collect()
115 }
116
117 pub fn tables(&self, key: &str) -> ConfigResult<Vec<ConfigView<'a>>> {
123 let Some(value) = self.get(key) else {
124 return Ok(Vec::new());
125 };
126 let Expr::List(items) = value else {
127 return Err(ConfigError::TypeMismatch {
128 key: key.to_owned(),
129 expected: "a table list",
130 });
131 };
132 items
133 .iter()
134 .enumerate()
135 .map(|(index, item)| match item {
136 Expr::Map(entries) => Ok(ConfigView::from_entries(entries)),
137 _ => Err(ConfigError::TypeMismatch {
138 key: format!("{key}[{index}]"),
139 expected: "a table",
140 }),
141 })
142 .collect()
143 }
144
145 fn required_error(&self, key: &str, expected: &'static str) -> ConfigError {
146 if self.get(key).is_some() {
147 ConfigError::TypeMismatch {
148 key: key.to_owned(),
149 expected,
150 }
151 } else {
152 ConfigError::MissingField {
153 key: key.to_owned(),
154 }
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use sim_kernel::Symbol;
162 use sim_value::build::{int, list, map, text};
163
164 use super::*;
165
166 #[test]
167 fn view_reads_typed_fields() {
168 let table = ConfigTable::new(
169 Symbol::qualified("model", "defaults"),
170 map(vec![
171 ("provider", text("modeled")),
172 ("enabled", Expr::Bool(true)),
173 ("limit", int(3)),
174 ]),
175 )
176 .unwrap();
177 let view = ConfigView::new(&table);
178
179 assert_eq!(view.required_string("provider").unwrap(), "modeled");
180 assert!(view.required_bool("enabled").unwrap());
181 assert_eq!(view.required_i64("limit").unwrap(), 3);
182 assert_eq!(
183 view.required_string("limit").unwrap_err(),
184 ConfigError::TypeMismatch {
185 key: "limit".to_owned(),
186 expected: "a string"
187 }
188 );
189 }
190
191 #[test]
192 fn view_reads_string_keyed_config_fields() {
193 let table = ConfigTable::new(
194 Symbol::qualified("model", "defaults"),
195 Expr::Map(vec![
196 (Expr::String("provider".to_owned()), text("modeled")),
197 (Expr::String("limit".to_owned()), int(3)),
198 ]),
199 )
200 .unwrap();
201 let view = ConfigView::new(&table);
202
203 assert_eq!(view.required_string("provider").unwrap(), "modeled");
204 assert_eq!(view.required_i64("limit").unwrap(), 3);
205 }
206
207 #[test]
208 fn view_reads_string_arrays_and_repeated_tables() {
209 let table = ConfigTable::new(
210 Symbol::qualified("sim", "cookbook"),
211 map(vec![
212 (
213 "minimum_loaded",
214 list(vec![text("codec/lisp"), text("numbers/i64")]),
215 ),
216 (
217 "loadable_lib",
218 list(vec![
219 map(vec![
220 ("id", text("numbers/i64")),
221 ("source", text("symbol:numbers/i64")),
222 ]),
223 map(vec![
224 ("id", text("numbers/cas")),
225 ("source", text("symbol:numbers/cas")),
226 ]),
227 ]),
228 ),
229 ]),
230 )
231 .unwrap();
232 let view = ConfigView::new(&table);
233
234 assert_eq!(
235 view.string_array("minimum_loaded").unwrap(),
236 ["codec/lisp", "numbers/i64"]
237 );
238 let loadable = view.tables("loadable_lib").unwrap();
239 assert_eq!(loadable.len(), 2);
240 assert_eq!(loadable[0].string("id"), Some("numbers/i64"));
241 assert_eq!(loadable[1].string("source"), Some("symbol:numbers/cas"));
242 assert!(view.string_array("missing").unwrap().is_empty());
243 assert!(view.tables("missing").unwrap().is_empty());
244 }
245
246 #[test]
247 fn view_reports_array_shape_errors() {
248 let table = ConfigTable::new(
249 Symbol::qualified("sim", "cookbook"),
250 map(vec![
251 ("minimum_loaded", text("codec/lisp")),
252 ("loadable_lib", list(vec![text("bad")])),
253 ]),
254 )
255 .unwrap();
256 let view = ConfigView::new(&table);
257
258 assert_eq!(
259 view.string_array("minimum_loaded").unwrap_err(),
260 ConfigError::TypeMismatch {
261 key: "minimum_loaded".to_owned(),
262 expected: "a string list"
263 }
264 );
265 assert_eq!(
266 view.tables("loadable_lib").unwrap_err(),
267 ConfigError::TypeMismatch {
268 key: "loadable_lib[0]".to_owned(),
269 expected: "a table"
270 }
271 );
272 }
273}