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        if f.alternate() {
176            writeln!(f, "{{")?;
177            for index in 0..self.entries.len() {
178                if index > 0 {
179                    writeln!(f, ",")?;
180                }
181                let (key, value) = &self.entries[index];
182                write!(f, "  {key:?}:")?;
183                writeln!(f)?;
184                write!(f, "  {value:#}")?;
185            }
186            writeln!(f)?;
187            write!(f, "}}")
188        } else {
189            write!(f, "{{")?;
190            for index in 0..self.entries.len() {
191                if index > 0 {
192                    write!(f, ", ")?;
193                }
194                let (key, value) = &self.entries[index];
195                write!(f, "{key:?}: {value}")?;
196            }
197            write!(f, "}}")
198        }
199    }
200}
201
202/// Dynamically typed configuration value (six variants, no null).
203#[derive(Debug, Clone, PartialEq)]
204pub enum Value {
205    Bool(bool),
206    Int(isize),
207    Float(f64),
208    String(String),
209    List(Vec<LocatedValue>),
210    Map(Map),
211}
212
213/// A [`Value`] with its [`Location`].
214///
215/// [`Display`] is compact by default; use `{value:#}` for a multiline dump with
216/// `@source:resource:line:column` on the first line.
217#[derive(Debug, Clone, PartialEq)]
218pub struct LocatedValue {
219    pub value: Value,
220    pub location: Location,
221}
222
223impl Display for LocatedValue {
224    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
225        if f.alternate() {
226            writeln!(f, "@{}", self.location)?;
227            write!(f, "{:#}", self.value)
228        } else {
229            write!(f, "{}", self.value)
230        }
231    }
232}
233
234impl Value {
235    pub fn new_map() -> Self {
236        Self::Map(Map::new())
237    }
238
239    pub fn new_list() -> Self {
240        Self::List(Vec::new())
241    }
242
243    pub fn new_string() -> Self {
244        Self::String(String::new())
245    }
246
247    pub fn is_bool(&self) -> bool {
248        matches!(self, Self::Bool(_))
249    }
250
251    pub fn as_bool(&self) -> Option<bool> {
252        match self {
253            Self::Bool(value) => Some(*value),
254            _ => None,
255        }
256    }
257
258    pub fn into_bool(self) -> Option<bool> {
259        match self {
260            Self::Bool(value) => Some(value),
261            _ => None,
262        }
263    }
264
265    pub fn bool_mut(&mut self) -> Option<&mut bool> {
266        match self {
267            Self::Bool(value) => Some(value),
268            _ => None,
269        }
270    }
271
272    pub fn is_int(&self) -> bool {
273        matches!(self, Self::Int(_))
274    }
275
276    pub fn as_int(&self) -> Option<isize> {
277        match self {
278            Self::Int(value) => Some(*value),
279            _ => None,
280        }
281    }
282
283    pub fn into_int(self) -> Option<isize> {
284        match self {
285            Self::Int(value) => Some(value),
286            _ => None,
287        }
288    }
289
290    pub fn int_mut(&mut self) -> Option<&mut isize> {
291        match self {
292            Self::Int(value) => Some(value),
293            _ => None,
294        }
295    }
296
297    pub fn is_float(&self) -> bool {
298        matches!(self, Self::Float(_))
299    }
300
301    pub fn as_float(&self) -> Option<f64> {
302        match self {
303            Self::Float(value) => Some(*value),
304            _ => None,
305        }
306    }
307
308    pub fn into_float(self) -> Option<f64> {
309        match self {
310            Self::Float(value) => Some(value),
311            _ => None,
312        }
313    }
314
315    pub fn float_mut(&mut self) -> Option<&mut f64> {
316        match self {
317            Self::Float(value) => Some(value),
318            _ => None,
319        }
320    }
321
322    pub fn is_string(&self) -> bool {
323        matches!(self, Self::String(_))
324    }
325
326    pub fn as_string(&self) -> Option<&String> {
327        match self {
328            Self::String(value) => Some(value),
329            _ => None,
330        }
331    }
332
333    pub fn into_string(self) -> Option<String> {
334        match self {
335            Self::String(value) => Some(value),
336            _ => None,
337        }
338    }
339
340    pub fn string_mut(&mut self) -> Option<&mut String> {
341        match self {
342            Self::String(value) => Some(value),
343            _ => None,
344        }
345    }
346
347    pub fn is_list(&self) -> bool {
348        matches!(self, Self::List(_))
349    }
350
351    pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
352        match self {
353            Self::List(value) => Some(value),
354            _ => None,
355        }
356    }
357
358    pub fn into_list(self) -> Option<Vec<LocatedValue>> {
359        match self {
360            Self::List(value) => Some(value),
361            _ => None,
362        }
363    }
364
365    pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
366        match self {
367            Self::List(value) => Some(value),
368            _ => None,
369        }
370    }
371
372    pub fn is_map(&self) -> bool {
373        matches!(self, Self::Map(_))
374    }
375
376    pub fn as_map(&self) -> Option<&Map> {
377        match self {
378            Self::Map(value) => Some(value),
379            _ => None,
380        }
381    }
382
383    pub fn into_map(self) -> Option<Map> {
384        match self {
385            Self::Map(value) => Some(value),
386            _ => None,
387        }
388    }
389
390    pub fn map_mut(&mut self) -> Option<&mut Map> {
391        match self {
392            Self::Map(value) => Some(value),
393            _ => None,
394        }
395    }
396
397    pub fn type_name(&self) -> ValueType {
398        match self {
399            Self::Bool(_) => ValueType::Bool,
400            Self::Int(_) => ValueType::Int,
401            Self::Float(_) => ValueType::Float,
402            Self::String(_) => ValueType::String,
403            Self::List(_) => ValueType::List,
404            Self::Map(_) => ValueType::Map,
405        }
406    }
407}
408
409impl Display for Value {
410    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
411        match self {
412            Self::Bool(value) => write!(f, "{value}"),
413            Self::Int(value) => write!(f, "{value}"),
414            Self::Float(value) => write!(f, "{value}"),
415            Self::String(value) => write!(f, "{value:?}"),
416            Self::List(values) => {
417                if f.alternate() {
418                    writeln!(f, "[")?;
419                    for (index, value) in values.iter().enumerate() {
420                        if index > 0 {
421                            writeln!(f, ",")?;
422                        }
423                        write!(f, "  {value:#}")?;
424                    }
425                    writeln!(f)?;
426                    write!(f, "]")
427                } else {
428                    write!(f, "[")?;
429                    let mut first = true;
430                    for value in values {
431                        if !first {
432                            write!(f, ", ")?;
433                        }
434                        write!(f, "{value}")?;
435                        first = false;
436                    }
437                    write!(f, "]")
438                }
439            }
440            Self::Map(value) => {
441                if f.alternate() {
442                    write!(f, "{value:#}")
443                } else {
444                    write!(f, "{value}")
445                }
446            }
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    fn located_string(text: &str) -> LocatedValue {
456        LocatedValue {
457            value: Value::String(text.to_string()),
458            location: Location::at("file", "test", None, None, None),
459        }
460    }
461
462    #[test]
463    fn last_key_wins() {
464        let mut map = Map::new();
465        map.insert("foo".to_string(), located_string("first"));
466        map.insert("foo".to_string(), located_string("second"));
467        assert_eq!(map.get("foo").unwrap().value.as_string().unwrap(), "second");
468    }
469
470    #[test]
471    fn default_display_is_compact() {
472        let value = LocatedValue {
473            value: Value::String("hello".to_string()),
474            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
475        };
476        let message = value.to_string();
477        assert!(!message.contains('\n'));
478        assert!(!message.starts_with('@'));
479        assert_eq!(message, "\"hello\"");
480    }
481
482    #[test]
483    fn alternate_display_shows_location_and_multiline() {
484        let value = LocatedValue {
485            value: Value::String("hello".to_string()),
486            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
487        };
488        let message = format!("{value:#}");
489        assert!(message.starts_with("@file:config.yaml:2:5\n"));
490        assert!(message.contains("\"hello\""));
491    }
492}