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}
104
105impl Display for ValueType {
106    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107        f.write_str(match self {
108            Self::Bool => "boolean",
109            Self::Int => "integer",
110            Self::Float => "float",
111            Self::String => "string",
112            Self::List => "list",
113            Self::Map => "map",
114        })
115    }
116}
117
118/// Ordered map of configuration keys to located values (last key wins on lookup).
119#[derive(Debug, Clone, PartialEq, Default)]
120pub struct Map {
121    entries: Vec<(String, LocatedValue)>,
122}
123
124impl Map {
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    pub fn len(&self) -> usize {
130        self.entries.len()
131    }
132
133    pub fn is_empty(&self) -> bool {
134        self.entries.is_empty()
135    }
136
137    pub fn contains_key(&self, key: &str) -> bool {
138        for index in (0..self.entries.len()).rev() {
139            if self.entries[index].0 == key {
140                return true;
141            }
142        }
143        false
144    }
145
146    pub fn get(&self, key: &str) -> Option<&LocatedValue> {
147        for index in (0..self.entries.len()).rev() {
148            if self.entries[index].0 == key {
149                return Some(&self.entries[index].1);
150            }
151        }
152        None
153    }
154
155    pub fn get_mut(&mut self, key: &str) -> Option<&mut LocatedValue> {
156        let mut found = None;
157        for index in (0..self.entries.len()).rev() {
158            if self.entries[index].0 == key {
159                found = Some(index);
160                break;
161            }
162        }
163        if let Some(index) = found {
164            Some(&mut self.entries[index].1)
165        } else {
166            None
167        }
168    }
169
170    pub fn insert(&mut self, key: String, value: LocatedValue) -> Option<LocatedValue> {
171        let old = self.remove(&key);
172        self.entries.push((key, value));
173        old
174    }
175
176    pub fn remove(&mut self, key: &str) -> Option<LocatedValue> {
177        let mut found = None;
178        for index in (0..self.entries.len()).rev() {
179            if self.entries[index].0 == key {
180                found = Some(index);
181                break;
182            }
183        }
184        if let Some(index) = found {
185            Some(self.entries.remove(index).1)
186        } else {
187            None
188        }
189    }
190
191    pub fn entries(&self) -> &[(String, LocatedValue)] {
192        &self.entries
193    }
194
195    pub fn entries_mut(&mut self) -> &mut Vec<(String, LocatedValue)> {
196        &mut self.entries
197    }
198}
199
200impl Display for Map {
201    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
202        let alternate = f.alternate();
203        let mut map = f.debug_map();
204        for (key, value) in &self.entries {
205            if alternate {
206                map.entry(key, &format_args!("{:#}", value));
207            } else {
208                map.entry(key, &format_args!("{}", value));
209            }
210        }
211        map.finish()
212    }
213}
214
215/// Dynamically typed configuration value (six variants, no null).
216#[derive(Debug, Clone, PartialEq)]
217pub enum Value {
218    Bool(bool),
219    Int(isize),
220    Float(f64),
221    String(String),
222    List(Vec<LocatedValue>),
223    Map(Map),
224}
225
226/// A [`Value`] with its [`Location`].
227///
228/// [`Display`] is compact by default; use `{value:#}` for a multiline dump with
229/// `@source:resource:line:column` on the first line.
230#[derive(Debug, Clone, PartialEq)]
231pub struct LocatedValue {
232    pub value: Value,
233    pub location: Location,
234}
235
236impl Display for LocatedValue {
237    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
238        if f.alternate() {
239            let mut map = f.debug_map();
240            map.entry(&"value", &format_args!("{:#}", self.value));
241            map.entry(
242                &"location",
243                &format_args!("{:?}", self.location.to_string()),
244            );
245            map.finish()
246        } else {
247            write!(f, "{}", self.value)
248        }
249    }
250}
251
252impl AsRef<Value> for Value {
253    fn as_ref(&self) -> &Value {
254        self
255    }
256}
257
258impl AsRef<Value> for LocatedValue {
259    fn as_ref(&self) -> &Value {
260        &self.value
261    }
262}
263
264impl Value {
265    pub fn new_map() -> Self {
266        Self::Map(Map::new())
267    }
268
269    pub fn new_list() -> Self {
270        Self::List(Vec::new())
271    }
272
273    pub fn new_string() -> Self {
274        Self::String(String::new())
275    }
276
277    pub fn is_bool(&self) -> bool {
278        matches!(self, Self::Bool(_))
279    }
280
281    pub fn as_bool(&self) -> Option<bool> {
282        match self {
283            Self::Bool(value) => Some(*value),
284            _ => None,
285        }
286    }
287
288    pub fn into_bool(self) -> Option<bool> {
289        match self {
290            Self::Bool(value) => Some(value),
291            _ => None,
292        }
293    }
294
295    pub fn bool_mut(&mut self) -> Option<&mut bool> {
296        match self {
297            Self::Bool(value) => Some(value),
298            _ => None,
299        }
300    }
301
302    pub fn is_int(&self) -> bool {
303        matches!(self, Self::Int(_))
304    }
305
306    pub fn as_int(&self) -> Option<isize> {
307        match self {
308            Self::Int(value) => Some(*value),
309            _ => None,
310        }
311    }
312
313    pub fn into_int(self) -> Option<isize> {
314        match self {
315            Self::Int(value) => Some(value),
316            _ => None,
317        }
318    }
319
320    pub fn int_mut(&mut self) -> Option<&mut isize> {
321        match self {
322            Self::Int(value) => Some(value),
323            _ => None,
324        }
325    }
326
327    pub fn is_float(&self) -> bool {
328        matches!(self, Self::Float(_))
329    }
330
331    pub fn as_float(&self) -> Option<f64> {
332        match self {
333            Self::Float(value) => Some(*value),
334            _ => None,
335        }
336    }
337
338    pub fn into_float(self) -> Option<f64> {
339        match self {
340            Self::Float(value) => Some(value),
341            _ => None,
342        }
343    }
344
345    pub fn float_mut(&mut self) -> Option<&mut f64> {
346        match self {
347            Self::Float(value) => Some(value),
348            _ => None,
349        }
350    }
351
352    pub fn is_string(&self) -> bool {
353        matches!(self, Self::String(_))
354    }
355
356    pub fn as_string(&self) -> Option<&String> {
357        match self {
358            Self::String(value) => Some(value),
359            _ => None,
360        }
361    }
362
363    pub fn into_string(self) -> Option<String> {
364        match self {
365            Self::String(value) => Some(value),
366            _ => None,
367        }
368    }
369
370    pub fn string_mut(&mut self) -> Option<&mut String> {
371        match self {
372            Self::String(value) => Some(value),
373            _ => None,
374        }
375    }
376
377    pub fn is_list(&self) -> bool {
378        matches!(self, Self::List(_))
379    }
380
381    pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
382        match self {
383            Self::List(value) => Some(value),
384            _ => None,
385        }
386    }
387
388    pub fn into_list(self) -> Option<Vec<LocatedValue>> {
389        match self {
390            Self::List(value) => Some(value),
391            _ => None,
392        }
393    }
394
395    pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
396        match self {
397            Self::List(value) => Some(value),
398            _ => None,
399        }
400    }
401
402    pub fn is_map(&self) -> bool {
403        matches!(self, Self::Map(_))
404    }
405
406    pub fn as_map(&self) -> Option<&Map> {
407        match self {
408            Self::Map(value) => Some(value),
409            _ => None,
410        }
411    }
412
413    pub fn into_map(self) -> Option<Map> {
414        match self {
415            Self::Map(value) => Some(value),
416            _ => None,
417        }
418    }
419
420    pub fn map_mut(&mut self) -> Option<&mut Map> {
421        match self {
422            Self::Map(value) => Some(value),
423            _ => None,
424        }
425    }
426
427    pub fn type_name(&self) -> ValueType {
428        match self {
429            Self::Bool(_) => ValueType::Bool,
430            Self::Int(_) => ValueType::Int,
431            Self::Float(_) => ValueType::Float,
432            Self::String(_) => ValueType::String,
433            Self::List(_) => ValueType::List,
434            Self::Map(_) => ValueType::Map,
435        }
436    }
437}
438
439impl Display for Value {
440    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
441        match self {
442            Self::Bool(value) => write!(f, "{value}"),
443            Self::Int(value) => write!(f, "{value}"),
444            Self::Float(value) => write!(f, "{value}"),
445            Self::String(value) => write!(f, "{value:?}"),
446            Self::List(values) => {
447                let alternate = f.alternate();
448                let mut list = f.debug_list();
449                for value in values {
450                    if alternate {
451                        list.entry(&format_args!("{:#}", value));
452                    } else {
453                        list.entry(&format_args!("{}", value));
454                    }
455                }
456                list.finish()
457            }
458            Self::Map(value) => Display::fmt(value, f),
459        }
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    fn located_string(text: &str) -> LocatedValue {
468        LocatedValue {
469            value: Value::String(text.to_string()),
470            location: Location::at("file", "test", None, None, None),
471        }
472    }
473
474    #[test]
475    fn as_ref_value_accepts_all_forms() {
476        fn take<V: AsRef<Value>>(value: V) -> Value {
477            value.as_ref().clone()
478        }
479        let value = Value::Int(7);
480        let located = LocatedValue {
481            value: Value::Int(7),
482            location: Location::at("file", "test", None, None, None),
483        };
484        assert_eq!(take(value.clone()), value); // Value
485        assert_eq!(take(&value), value); // &Value
486        assert_eq!(take(located.clone()), value); // LocatedValue
487        assert_eq!(take(&located), value); // &LocatedValue
488    }
489
490    #[test]
491    fn last_key_wins() {
492        let mut map = Map::new();
493        map.insert("foo".to_string(), located_string("first"));
494        map.insert("foo".to_string(), located_string("second"));
495        assert_eq!(map.get("foo").unwrap().value.as_string().unwrap(), "second");
496    }
497
498    #[test]
499    fn default_display_is_compact() {
500        let value = LocatedValue {
501            value: Value::String("hello".to_string()),
502            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
503        };
504        let message = value.to_string();
505        assert!(!message.contains('\n'));
506        assert!(!message.starts_with('@'));
507        assert_eq!(message, "\"hello\"");
508    }
509
510    #[test]
511    fn alternate_display_shows_location_and_multiline() {
512        let value = LocatedValue {
513            value: Value::String("hello".to_string()),
514            location: Location::at("file", "config.yaml", Some(2), Some(5), None),
515        };
516        let message = format!("{value:#}");
517        assert_eq!(
518            message,
519            "{\n    \"value\": \"hello\",\n    \"location\": \"file:config.yaml:2:5\",\n}"
520        );
521        assert!(!message.contains('@'));
522    }
523
524    #[test]
525    fn value_accessors_and_constructors() {
526        let mut value = Value::Bool(true);
527        assert!(value.is_bool());
528        assert_eq!(value.as_bool(), Some(true));
529        assert_eq!(value.type_name(), ValueType::Bool);
530        if let Some(flag) = value.bool_mut() {
531            *flag = false;
532        }
533        assert_eq!(value.into_bool(), Some(false));
534
535        let list = Value::new_list();
536        assert!(list.is_list());
537        let map = Value::new_map();
538        assert!(map.is_map());
539        let text = Value::new_string();
540        assert!(text.is_string());
541    }
542
543    #[test]
544    fn map_remove_get_mut_and_display() {
545        let mut map = Map::new();
546        map.insert("a".to_string(), located_string("one"));
547        map.insert("b".to_string(), located_string("two"));
548        assert_eq!(map.len(), 2);
549        assert!(map.contains_key("a"));
550        assert!(map.get_mut("b").is_some());
551        let removed = map.remove("a");
552        assert!(removed.is_some());
553        assert!(!map.contains_key("a"));
554
555        let compact = format!("{map}");
556        assert!(compact.contains("b"));
557        let detailed = format!("{map:#}");
558        assert!(detailed.contains("location"));
559    }
560
561    #[test]
562    fn location_display_and_with_length() {
563        let location = Location::at("file", "", Some(1), Some(2), None).with_length(3);
564        assert_eq!(location.to_string(), "file:1:2");
565        let resourceful = Location::at("file", "cfg.yml", Some(4), None, None);
566        assert_eq!(resourceful.to_string(), "file:cfg.yml:4");
567    }
568
569    #[test]
570    fn value_list_and_map_display_modes() {
571        let list = Value::List(vec![located_string("a"), located_string("b")]);
572        assert!(format!("{list}").contains("a"));
573        assert!(format!("{list:#}").contains("location"));
574
575        let mut map = Map::new();
576        map.insert("k".to_string(), located_string("v"));
577        let map_value = Value::Map(map);
578        assert!(format!("{map_value}").contains("k"));
579    }
580}