Skip to main content

tanzim_value/
value.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::num::NonZeroU32;
3use tanzim_source::Source;
4
5/// Source and optional position of a configuration value.
6///
7/// Holds the full originating [`Source`] (name, options, resource — including any `on_error`
8/// policy), so a value or error can be traced back to where and how it was declared. Positions are
9/// 1-based and stored as [`NonZeroU32`]; [`crate::Error`] boxes the [`Location`] so results stay
10/// small. Construct via [`Location::in_source`] (the real source) or [`Location::at`] (a bare
11/// name/resource, for synthetic origins).
12#[derive(Debug, Clone, PartialEq)]
13pub struct Location {
14    pub source: Source,
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    /// Build a location from the full originating [`Source`].
31    pub fn in_source(
32        source: Source,
33        line: Option<usize>,
34        column: Option<usize>,
35        length: Option<usize>,
36    ) -> Self {
37        Self {
38            source,
39            line: line.and_then(position),
40            column: column.and_then(position),
41            length: length.and_then(position),
42        }
43    }
44
45    /// Build a location from a bare source name and resource (a synthetic [`Source`] with no
46    /// options), for origins that do not come from parsing a real source string.
47    pub fn at(
48        source_name: &str,
49        resource: &str,
50        line: Option<usize>,
51        column: Option<usize>,
52        length: Option<usize>,
53    ) -> Self {
54        Self::in_source(
55            Source::named(source_name).with_resource(resource),
56            line,
57            column,
58            length,
59        )
60    }
61
62    /// The originating source's name (loader kind).
63    pub fn source_name(&self) -> &str {
64        self.source.source()
65    }
66
67    /// The originating source's resource (address).
68    pub fn resource(&self) -> &str {
69        self.source.resource()
70    }
71
72    pub fn with_length(mut self, length: usize) -> Self {
73        self.length = position(length);
74        self
75    }
76}
77
78impl Display for Location {
79    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80        let resource = self.source.resource();
81        if resource.is_empty() {
82            write!(f, "{}", self.source.source())?;
83        } else {
84            write!(f, "{}:{}", self.source.source(), resource)?;
85        }
86        match (self.line, self.column) {
87            (Some(line), Some(column)) => write!(f, ":{line}:{column}"),
88            (Some(line), None) => write!(f, ":{line}"),
89            _ => Ok(()),
90        }
91    }
92}
93
94/// Kind of value stored in [`Value`].
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum ValueType {
97    Bool,
98    Int,
99    Float,
100    String,
101    List,
102    Map,
103    Null,
104    Comment,
105}
106
107impl Display for ValueType {
108    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109        f.write_str(match self {
110            Self::Bool => "boolean",
111            Self::Int => "integer",
112            Self::Float => "float",
113            Self::String => "string",
114            Self::List => "list",
115            Self::Map => "map",
116            Self::Null => "null",
117            Self::Comment => "comment",
118        })
119    }
120}
121
122/// Ordered map of configuration keys to located values (last key wins on lookup).
123#[derive(Debug, Clone, PartialEq, Default)]
124pub struct Map {
125    entries: Vec<(String, LocatedValue)>,
126}
127
128impl Map {
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    pub fn len(&self) -> usize {
134        self.entries.len()
135    }
136
137    pub fn is_empty(&self) -> bool {
138        self.entries.is_empty()
139    }
140
141    pub fn contains_key(&self, key: &str) -> bool {
142        for index in (0..self.entries.len()).rev() {
143            if self.entries[index].0 == key {
144                return true;
145            }
146        }
147        false
148    }
149
150    pub fn get(&self, key: &str) -> Option<&LocatedValue> {
151        for index in (0..self.entries.len()).rev() {
152            if self.entries[index].0 == key {
153                return Some(&self.entries[index].1);
154            }
155        }
156        None
157    }
158
159    pub fn get_mut(&mut self, key: &str) -> Option<&mut LocatedValue> {
160        let mut found = None;
161        for index in (0..self.entries.len()).rev() {
162            if self.entries[index].0 == key {
163                found = Some(index);
164                break;
165            }
166        }
167        if let Some(index) = found {
168            Some(&mut self.entries[index].1)
169        } else {
170            None
171        }
172    }
173
174    pub fn insert(&mut self, key: String, value: LocatedValue) -> Option<LocatedValue> {
175        let old = self.remove(&key);
176        self.entries.push((key, value));
177        old
178    }
179
180    pub fn remove(&mut self, key: &str) -> Option<LocatedValue> {
181        let mut found = None;
182        for index in (0..self.entries.len()).rev() {
183            if self.entries[index].0 == key {
184                found = Some(index);
185                break;
186            }
187        }
188        if let Some(index) = found {
189            Some(self.entries.remove(index).1)
190        } else {
191            None
192        }
193    }
194
195    pub fn entries(&self) -> &[(String, LocatedValue)] {
196        &self.entries
197    }
198
199    pub fn entries_mut(&mut self) -> &mut Vec<(String, LocatedValue)> {
200        &mut self.entries
201    }
202}
203
204impl Display for Map {
205    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
206        let alternate = f.alternate();
207        let mut map = f.debug_map();
208        for (key, value) in &self.entries {
209            if alternate {
210                map.entry(key, &format_args!("{:#}", value));
211            } else {
212                map.entry(key, &format_args!("{}", value));
213            }
214        }
215        map.finish()
216    }
217}
218
219/// Dynamically typed configuration value.
220#[derive(Debug, Clone, PartialEq)]
221pub enum Value {
222    Bool(bool),
223    Int(isize),
224    Float(f64),
225    String(String),
226    List(Vec<LocatedValue>),
227    Map(Map),
228    Null,
229    Comment(String),
230}
231
232/// A [`Value`] with its [`Location`].
233///
234/// [`Display`] is compact by default; use `{value:#}` for a multiline dump with
235/// `@source:resource:line:column` on the first line.
236#[derive(Debug, Clone, PartialEq)]
237pub struct LocatedValue {
238    pub value: Value,
239    pub location: Location,
240}
241
242impl Display for LocatedValue {
243    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
244        if f.alternate() {
245            let mut map = f.debug_map();
246            map.entry(&"value", &format_args!("{:#}", self.value));
247            map.entry(
248                &"location",
249                &format_args!("{:?}", self.location.to_string()),
250            );
251            map.finish()
252        } else {
253            write!(f, "{}", self.value)
254        }
255    }
256}
257
258impl AsRef<Value> for Value {
259    fn as_ref(&self) -> &Value {
260        self
261    }
262}
263
264impl AsRef<Value> for LocatedValue {
265    fn as_ref(&self) -> &Value {
266        &self.value
267    }
268}
269
270impl Value {
271    pub fn new_map() -> Self {
272        Self::Map(Map::new())
273    }
274
275    pub fn new_list() -> Self {
276        Self::List(Vec::new())
277    }
278
279    pub fn new_string() -> Self {
280        Self::String(String::new())
281    }
282
283    pub fn is_bool(&self) -> bool {
284        matches!(self, Self::Bool(_))
285    }
286
287    pub fn as_bool(&self) -> Option<bool> {
288        match self {
289            Self::Bool(value) => Some(*value),
290            _ => None,
291        }
292    }
293
294    pub fn into_bool(self) -> Option<bool> {
295        match self {
296            Self::Bool(value) => Some(value),
297            _ => None,
298        }
299    }
300
301    pub fn bool_mut(&mut self) -> Option<&mut bool> {
302        match self {
303            Self::Bool(value) => Some(value),
304            _ => None,
305        }
306    }
307
308    pub fn is_int(&self) -> bool {
309        matches!(self, Self::Int(_))
310    }
311
312    pub fn as_int(&self) -> Option<isize> {
313        match self {
314            Self::Int(value) => Some(*value),
315            _ => None,
316        }
317    }
318
319    pub fn into_int(self) -> Option<isize> {
320        match self {
321            Self::Int(value) => Some(value),
322            _ => None,
323        }
324    }
325
326    pub fn int_mut(&mut self) -> Option<&mut isize> {
327        match self {
328            Self::Int(value) => Some(value),
329            _ => None,
330        }
331    }
332
333    pub fn is_float(&self) -> bool {
334        matches!(self, Self::Float(_))
335    }
336
337    pub fn as_float(&self) -> Option<f64> {
338        match self {
339            Self::Float(value) => Some(*value),
340            _ => None,
341        }
342    }
343
344    pub fn into_float(self) -> Option<f64> {
345        match self {
346            Self::Float(value) => Some(value),
347            _ => None,
348        }
349    }
350
351    pub fn float_mut(&mut self) -> Option<&mut f64> {
352        match self {
353            Self::Float(value) => Some(value),
354            _ => None,
355        }
356    }
357
358    pub fn is_string(&self) -> bool {
359        matches!(self, Self::String(_))
360    }
361
362    pub fn as_string(&self) -> Option<&String> {
363        match self {
364            Self::String(value) => Some(value),
365            _ => None,
366        }
367    }
368
369    pub fn into_string(self) -> Option<String> {
370        match self {
371            Self::String(value) => Some(value),
372            _ => None,
373        }
374    }
375
376    pub fn string_mut(&mut self) -> Option<&mut String> {
377        match self {
378            Self::String(value) => Some(value),
379            _ => None,
380        }
381    }
382
383    pub fn is_list(&self) -> bool {
384        matches!(self, Self::List(_))
385    }
386
387    pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
388        match self {
389            Self::List(value) => Some(value),
390            _ => None,
391        }
392    }
393
394    pub fn into_list(self) -> Option<Vec<LocatedValue>> {
395        match self {
396            Self::List(value) => Some(value),
397            _ => None,
398        }
399    }
400
401    pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
402        match self {
403            Self::List(value) => Some(value),
404            _ => None,
405        }
406    }
407
408    pub fn is_map(&self) -> bool {
409        matches!(self, Self::Map(_))
410    }
411
412    pub fn as_map(&self) -> Option<&Map> {
413        match self {
414            Self::Map(value) => Some(value),
415            _ => None,
416        }
417    }
418
419    pub fn into_map(self) -> Option<Map> {
420        match self {
421            Self::Map(value) => Some(value),
422            _ => None,
423        }
424    }
425
426    pub fn map_mut(&mut self) -> Option<&mut Map> {
427        match self {
428            Self::Map(value) => Some(value),
429            _ => None,
430        }
431    }
432
433    pub fn is_null(&self) -> bool {
434        matches!(self, Self::Null)
435    }
436
437    pub fn is_comment(&self) -> bool {
438        matches!(self, Self::Comment(_))
439    }
440
441    pub fn as_comment(&self) -> Option<&str> {
442        match self {
443            Self::Comment(value) => Some(value),
444            _ => None,
445        }
446    }
447
448    pub fn into_comment(self) -> Option<String> {
449        match self {
450            Self::Comment(value) => Some(value),
451            _ => None,
452        }
453    }
454
455    pub fn type_name(&self) -> ValueType {
456        match self {
457            Self::Bool(_) => ValueType::Bool,
458            Self::Int(_) => ValueType::Int,
459            Self::Float(_) => ValueType::Float,
460            Self::String(_) => ValueType::String,
461            Self::List(_) => ValueType::List,
462            Self::Map(_) => ValueType::Map,
463            Self::Null => ValueType::Null,
464            Self::Comment(_) => ValueType::Comment,
465        }
466    }
467}
468
469impl Display for Value {
470    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
471        match self {
472            Self::Bool(value) => write!(f, "{value}"),
473            Self::Int(value) => write!(f, "{value}"),
474            Self::Float(value) => write!(f, "{value}"),
475            Self::String(value) => write!(f, "{value:?}"),
476            Self::List(values) => {
477                let alternate = f.alternate();
478                let mut list = f.debug_list();
479                for value in values {
480                    if alternate {
481                        list.entry(&format_args!("{:#}", value));
482                    } else {
483                        list.entry(&format_args!("{}", value));
484                    }
485                }
486                list.finish()
487            }
488            Self::Map(value) => Display::fmt(value, f),
489            Self::Null => f.write_str("null"),
490            Self::Comment(value) => f.write_str(value),
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    fn located_string(text: &str) -> LocatedValue {
500        LocatedValue {
501            value: Value::String(text.to_string()),
502            location: Location::at("file", "test", None, None, None),
503        }
504    }
505
506    #[test]
507    fn as_ref_value_accepts_all_forms() {
508        fn take<V: AsRef<Value>>(value: V) -> Value {
509            value.as_ref().clone()
510        }
511        let value = Value::Int(7);
512        let located = LocatedValue {
513            value: Value::Int(7),
514            location: Location::at("file", "test", None, None, None),
515        };
516        assert_eq!(take(value.clone()), value); // Value
517        assert_eq!(take(&value), value); // &Value
518        assert_eq!(take(located.clone()), value); // LocatedValue
519        assert_eq!(take(&located), value); // &LocatedValue
520    }
521
522    #[test]
523    fn last_key_wins() {
524        let mut map = Map::new();
525        map.insert("foo".to_string(), located_string("first"));
526        map.insert("foo".to_string(), located_string("second"));
527        assert_eq!(map.get("foo").unwrap().value.as_string().unwrap(), "second");
528    }
529
530    #[test]
531    fn default_display_is_compact() {
532        let value = LocatedValue {
533            value: Value::String("hello".to_string()),
534            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
535        };
536        let message = value.to_string();
537        assert!(!message.contains('\n'));
538        assert!(!message.starts_with('@'));
539        assert_eq!(message, "\"hello\"");
540    }
541
542    #[test]
543    fn alternate_display_shows_location_and_multiline() {
544        let value = LocatedValue {
545            value: Value::String("hello".to_string()),
546            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
547        };
548        let message = format!("{value:#}");
549        assert_eq!(
550            message,
551            "{\n    \"value\": \"hello\",\n    \"location\": \"file:config.yaml:2:5\",\n}"
552        );
553        assert!(!message.contains('@'));
554    }
555
556    #[test]
557    fn value_accessors_and_constructors() {
558        let mut value = Value::Bool(true);
559        assert!(value.is_bool());
560        assert_eq!(value.as_bool(), Some(true));
561        assert_eq!(value.type_name(), ValueType::Bool);
562        if let Some(flag) = value.bool_mut() {
563            *flag = false;
564        }
565        assert_eq!(value.into_bool(), Some(false));
566
567        let list = Value::new_list();
568        assert!(list.is_list());
569        let map = Value::new_map();
570        assert!(map.is_map());
571        let text = Value::new_string();
572        assert!(text.is_string());
573    }
574
575    #[test]
576    fn map_remove_get_mut_and_display() {
577        let mut map = Map::new();
578        map.insert("a".to_string(), located_string("one"));
579        map.insert("b".to_string(), located_string("two"));
580        assert_eq!(map.len(), 2);
581        assert!(map.contains_key("a"));
582        assert!(map.get_mut("b").is_some());
583        let removed = map.remove("a");
584        assert!(removed.is_some());
585        assert!(!map.contains_key("a"));
586
587        let compact = format!("{map}");
588        assert!(compact.contains("b"));
589        let detailed = format!("{map:#}");
590        assert!(detailed.contains("location"));
591    }
592
593    #[test]
594    fn location_display_and_with_length() {
595        let location = Location::at("file", "", Some(1), Some(2), None).with_length(3);
596        assert_eq!(location.to_string(), "file:1:2");
597        let resourceful = Location::at("file", "cfg.yml", Some(4), None, None);
598        assert_eq!(resourceful.to_string(), "file:cfg.yml:4");
599    }
600
601    #[test]
602    fn value_list_and_map_display_modes() {
603        let list = Value::List(vec![located_string("a"), located_string("b")]);
604        assert!(format!("{list}").contains("a"));
605        assert!(format!("{list:#}").contains("location"));
606
607        let mut map = Map::new();
608        map.insert("k".to_string(), located_string("v"));
609        let map_value = Value::Map(map);
610        assert!(format!("{map_value}").contains("k"));
611    }
612}