Skip to main content

tanzim_value/
value.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::num::NonZeroU32;
3
4/// Source and optional position of a configuration value.
5///
6/// Positions are 1-based and stored as [`NonZeroU32`] so that the whole
7/// [`Location`] (and therefore [`crate::Error`]) stays small enough to return by
8/// value without triggering `clippy::result_large_err`. Construct via
9/// [`Location::at`], which accepts ordinary `usize` positions and discards any
10/// out-of-range or zero value as "absent".
11#[derive(Debug, Clone, PartialEq)]
12pub struct Location {
13    pub source_name: String,
14    pub resource: String,
15    pub line: Option<NonZeroU32>,
16    pub column: Option<NonZeroU32>,
17    /// UTF-8 character span length for error underlines; defaults to one caret.
18    pub length: Option<NonZeroU32>,
19}
20
21/// Convert a 1-based `usize` position into the compact [`NonZeroU32`] storage.
22///
23/// Returns `None` for zero (treated as "no position") and clamps values larger
24/// than [`u32::MAX`] to `u32::MAX` rather than overflowing.
25fn position(value: usize) -> Option<NonZeroU32> {
26    NonZeroU32::new(u32::try_from(value).unwrap_or(u32::MAX))
27}
28
29impl Location {
30    pub fn at(
31        source_name: &str,
32        resource: &str,
33        line: Option<usize>,
34        column: Option<usize>,
35        length: Option<usize>,
36    ) -> Self {
37        Self {
38            source_name: source_name.to_string(),
39            resource: resource.to_string(),
40            line: line.and_then(position),
41            column: column.and_then(position),
42            length: length.and_then(position),
43        }
44    }
45
46    pub fn with_length(mut self, length: usize) -> Self {
47        self.length = position(length);
48        self
49    }
50}
51
52impl Display for Location {
53    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54        if self.resource.is_empty() {
55            write!(f, "{}", self.source_name)?;
56        } else {
57            write!(f, "{}:{}", self.source_name, self.resource)?;
58        }
59        match (self.line, self.column) {
60            (Some(line), Some(column)) => write!(f, ":{line}:{column}"),
61            (Some(line), None) => write!(f, ":{line}"),
62            _ => Ok(()),
63        }
64    }
65}
66
67/// Kind of value stored in [`Value`].
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum ValueType {
70    Bool,
71    Int,
72    Float,
73    String,
74    List,
75    Map,
76}
77
78impl Display for ValueType {
79    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80        f.write_str(match self {
81            Self::Bool => "boolean",
82            Self::Int => "integer",
83            Self::Float => "float",
84            Self::String => "string",
85            Self::List => "list",
86            Self::Map => "map",
87        })
88    }
89}
90
91/// Ordered map of configuration keys to located values (last key wins on lookup).
92#[derive(Debug, Clone, PartialEq, Default)]
93pub struct Map {
94    entries: Vec<(String, LocatedValue)>,
95}
96
97impl Map {
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    pub fn len(&self) -> usize {
103        self.entries.len()
104    }
105
106    pub fn is_empty(&self) -> bool {
107        self.entries.is_empty()
108    }
109
110    pub fn contains_key(&self, key: &str) -> bool {
111        for index in (0..self.entries.len()).rev() {
112            if self.entries[index].0 == key {
113                return true;
114            }
115        }
116        false
117    }
118
119    pub fn get(&self, key: &str) -> Option<&LocatedValue> {
120        for index in (0..self.entries.len()).rev() {
121            if self.entries[index].0 == key {
122                return Some(&self.entries[index].1);
123            }
124        }
125        None
126    }
127
128    pub fn get_mut(&mut self, key: &str) -> Option<&mut LocatedValue> {
129        let mut found = None;
130        for index in (0..self.entries.len()).rev() {
131            if self.entries[index].0 == key {
132                found = Some(index);
133                break;
134            }
135        }
136        if let Some(index) = found {
137            Some(&mut self.entries[index].1)
138        } else {
139            None
140        }
141    }
142
143    pub fn insert(&mut self, key: String, value: LocatedValue) -> Option<LocatedValue> {
144        let old = self.remove(&key);
145        self.entries.push((key, value));
146        old
147    }
148
149    pub fn remove(&mut self, key: &str) -> Option<LocatedValue> {
150        let mut found = None;
151        for index in (0..self.entries.len()).rev() {
152            if self.entries[index].0 == key {
153                found = Some(index);
154                break;
155            }
156        }
157        if let Some(index) = found {
158            Some(self.entries.remove(index).1)
159        } else {
160            None
161        }
162    }
163
164    pub fn entries(&self) -> &[(String, LocatedValue)] {
165        &self.entries
166    }
167
168    pub fn entries_mut(&mut self) -> &mut Vec<(String, LocatedValue)> {
169        &mut self.entries
170    }
171}
172
173impl Display for Map {
174    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
175        let alternate = f.alternate();
176        let mut map = f.debug_map();
177        for (key, value) in &self.entries {
178            if alternate {
179                map.entry(key, &format_args!("{:#}", value));
180            } else {
181                map.entry(key, &format_args!("{}", value));
182            }
183        }
184        map.finish()
185    }
186}
187
188/// Dynamically typed configuration value (six variants, no null).
189#[derive(Debug, Clone, PartialEq)]
190pub enum Value {
191    Bool(bool),
192    Int(isize),
193    Float(f64),
194    String(String),
195    List(Vec<LocatedValue>),
196    Map(Map),
197}
198
199/// A [`Value`] with its [`Location`].
200///
201/// [`Display`] is compact by default; use `{value:#}` for a multiline dump with
202/// `@source:resource:line:column` on the first line.
203#[derive(Debug, Clone, PartialEq)]
204pub struct LocatedValue {
205    pub value: Value,
206    pub location: Location,
207}
208
209impl Display for LocatedValue {
210    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
211        if f.alternate() {
212            let mut map = f.debug_map();
213            map.entry(&"value", &format_args!("{:#}", self.value));
214            map.entry(
215                &"location",
216                &format_args!("{:?}", self.location.to_string()),
217            );
218            map.finish()
219        } else {
220            write!(f, "{}", self.value)
221        }
222    }
223}
224
225impl AsRef<Value> for Value {
226    fn as_ref(&self) -> &Value {
227        self
228    }
229}
230
231impl AsRef<Value> for LocatedValue {
232    fn as_ref(&self) -> &Value {
233        &self.value
234    }
235}
236
237impl Value {
238    pub fn new_map() -> Self {
239        Self::Map(Map::new())
240    }
241
242    pub fn new_list() -> Self {
243        Self::List(Vec::new())
244    }
245
246    pub fn new_string() -> Self {
247        Self::String(String::new())
248    }
249
250    pub fn is_bool(&self) -> bool {
251        matches!(self, Self::Bool(_))
252    }
253
254    pub fn as_bool(&self) -> Option<bool> {
255        match self {
256            Self::Bool(value) => Some(*value),
257            _ => None,
258        }
259    }
260
261    pub fn into_bool(self) -> Option<bool> {
262        match self {
263            Self::Bool(value) => Some(value),
264            _ => None,
265        }
266    }
267
268    pub fn bool_mut(&mut self) -> Option<&mut bool> {
269        match self {
270            Self::Bool(value) => Some(value),
271            _ => None,
272        }
273    }
274
275    pub fn is_int(&self) -> bool {
276        matches!(self, Self::Int(_))
277    }
278
279    pub fn as_int(&self) -> Option<isize> {
280        match self {
281            Self::Int(value) => Some(*value),
282            _ => None,
283        }
284    }
285
286    pub fn into_int(self) -> Option<isize> {
287        match self {
288            Self::Int(value) => Some(value),
289            _ => None,
290        }
291    }
292
293    pub fn int_mut(&mut self) -> Option<&mut isize> {
294        match self {
295            Self::Int(value) => Some(value),
296            _ => None,
297        }
298    }
299
300    pub fn is_float(&self) -> bool {
301        matches!(self, Self::Float(_))
302    }
303
304    pub fn as_float(&self) -> Option<f64> {
305        match self {
306            Self::Float(value) => Some(*value),
307            _ => None,
308        }
309    }
310
311    pub fn into_float(self) -> Option<f64> {
312        match self {
313            Self::Float(value) => Some(value),
314            _ => None,
315        }
316    }
317
318    pub fn float_mut(&mut self) -> Option<&mut f64> {
319        match self {
320            Self::Float(value) => Some(value),
321            _ => None,
322        }
323    }
324
325    pub fn is_string(&self) -> bool {
326        matches!(self, Self::String(_))
327    }
328
329    pub fn as_string(&self) -> Option<&String> {
330        match self {
331            Self::String(value) => Some(value),
332            _ => None,
333        }
334    }
335
336    pub fn into_string(self) -> Option<String> {
337        match self {
338            Self::String(value) => Some(value),
339            _ => None,
340        }
341    }
342
343    pub fn string_mut(&mut self) -> Option<&mut String> {
344        match self {
345            Self::String(value) => Some(value),
346            _ => None,
347        }
348    }
349
350    pub fn is_list(&self) -> bool {
351        matches!(self, Self::List(_))
352    }
353
354    pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
355        match self {
356            Self::List(value) => Some(value),
357            _ => None,
358        }
359    }
360
361    pub fn into_list(self) -> Option<Vec<LocatedValue>> {
362        match self {
363            Self::List(value) => Some(value),
364            _ => None,
365        }
366    }
367
368    pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
369        match self {
370            Self::List(value) => Some(value),
371            _ => None,
372        }
373    }
374
375    pub fn is_map(&self) -> bool {
376        matches!(self, Self::Map(_))
377    }
378
379    pub fn as_map(&self) -> Option<&Map> {
380        match self {
381            Self::Map(value) => Some(value),
382            _ => None,
383        }
384    }
385
386    pub fn into_map(self) -> Option<Map> {
387        match self {
388            Self::Map(value) => Some(value),
389            _ => None,
390        }
391    }
392
393    pub fn map_mut(&mut self) -> Option<&mut Map> {
394        match self {
395            Self::Map(value) => Some(value),
396            _ => None,
397        }
398    }
399
400    pub fn type_name(&self) -> ValueType {
401        match self {
402            Self::Bool(_) => ValueType::Bool,
403            Self::Int(_) => ValueType::Int,
404            Self::Float(_) => ValueType::Float,
405            Self::String(_) => ValueType::String,
406            Self::List(_) => ValueType::List,
407            Self::Map(_) => ValueType::Map,
408        }
409    }
410}
411
412impl Display for Value {
413    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
414        match self {
415            Self::Bool(value) => write!(f, "{value}"),
416            Self::Int(value) => write!(f, "{value}"),
417            Self::Float(value) => write!(f, "{value}"),
418            Self::String(value) => write!(f, "{value:?}"),
419            Self::List(values) => {
420                let alternate = f.alternate();
421                let mut list = f.debug_list();
422                for value in values {
423                    if alternate {
424                        list.entry(&format_args!("{:#}", value));
425                    } else {
426                        list.entry(&format_args!("{}", value));
427                    }
428                }
429                list.finish()
430            }
431            Self::Map(value) => Display::fmt(value, f),
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    fn located_string(text: &str) -> LocatedValue {
441        LocatedValue {
442            value: Value::String(text.to_string()),
443            location: Location::at("file", "test", None, None, None),
444        }
445    }
446
447    #[test]
448    fn as_ref_value_accepts_all_forms() {
449        fn take<V: AsRef<Value>>(value: V) -> Value {
450            value.as_ref().clone()
451        }
452        let value = Value::Int(7);
453        let located = LocatedValue {
454            value: Value::Int(7),
455            location: Location::at("file", "test", None, None, None),
456        };
457        assert_eq!(take(value.clone()), value); // Value
458        assert_eq!(take(&value), value); // &Value
459        assert_eq!(take(located.clone()), value); // LocatedValue
460        assert_eq!(take(&located), value); // &LocatedValue
461    }
462
463    #[test]
464    fn last_key_wins() {
465        let mut map = Map::new();
466        map.insert("foo".to_string(), located_string("first"));
467        map.insert("foo".to_string(), located_string("second"));
468        assert_eq!(map.get("foo").unwrap().value.as_string().unwrap(), "second");
469    }
470
471    #[test]
472    fn default_display_is_compact() {
473        let value = LocatedValue {
474            value: Value::String("hello".to_string()),
475            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
476        };
477        let message = value.to_string();
478        assert!(!message.contains('\n'));
479        assert!(!message.starts_with('@'));
480        assert_eq!(message, "\"hello\"");
481    }
482
483    #[test]
484    fn alternate_display_shows_location_and_multiline() {
485        let value = LocatedValue {
486            value: Value::String("hello".to_string()),
487            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
488        };
489        let message = format!("{value:#}");
490        assert_eq!(
491            message,
492            "{\n    \"value\": \"hello\",\n    \"location\": \"file:config.yaml:2:5\",\n}"
493        );
494        assert!(!message.contains('@'));
495    }
496}