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}
105
106impl Display for ValueType {
107    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108        f.write_str(match self {
109            Self::Bool => "boolean",
110            Self::Int => "integer",
111            Self::Float => "float",
112            Self::String => "string",
113            Self::List => "list",
114            Self::Map => "map",
115            Self::Null => "null",
116        })
117    }
118}
119
120/// Ordered map of configuration keys to located values (last key wins on lookup).
121#[derive(Debug, Clone, PartialEq, Default)]
122pub struct Map {
123    entries: Vec<(String, LocatedValue)>,
124}
125
126impl Map {
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    pub fn len(&self) -> usize {
132        self.entries.len()
133    }
134
135    pub fn is_empty(&self) -> bool {
136        self.entries.is_empty()
137    }
138
139    pub fn contains_key(&self, key: &str) -> bool {
140        for index in (0..self.entries.len()).rev() {
141            if self.entries[index].0 == key {
142                return true;
143            }
144        }
145        false
146    }
147
148    pub fn get(&self, key: &str) -> Option<&LocatedValue> {
149        for index in (0..self.entries.len()).rev() {
150            if self.entries[index].0 == key {
151                return Some(&self.entries[index].1);
152            }
153        }
154        None
155    }
156
157    pub fn get_mut(&mut self, key: &str) -> Option<&mut LocatedValue> {
158        let mut found = None;
159        for index in (0..self.entries.len()).rev() {
160            if self.entries[index].0 == key {
161                found = Some(index);
162                break;
163            }
164        }
165        if let Some(index) = found {
166            Some(&mut self.entries[index].1)
167        } else {
168            None
169        }
170    }
171
172    pub fn insert(&mut self, key: String, value: LocatedValue) -> Option<LocatedValue> {
173        let old = self.remove(&key);
174        self.entries.push((key, value));
175        old
176    }
177
178    pub fn remove(&mut self, key: &str) -> Option<LocatedValue> {
179        let mut found = None;
180        for index in (0..self.entries.len()).rev() {
181            if self.entries[index].0 == key {
182                found = Some(index);
183                break;
184            }
185        }
186        if let Some(index) = found {
187            Some(self.entries.remove(index).1)
188        } else {
189            None
190        }
191    }
192
193    pub fn entries(&self) -> &[(String, LocatedValue)] {
194        &self.entries
195    }
196
197    pub fn entries_mut(&mut self) -> &mut Vec<(String, LocatedValue)> {
198        &mut self.entries
199    }
200}
201
202impl Display for Map {
203    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
204        let alternate = f.alternate();
205        let mut map = f.debug_map();
206        for (key, value) in &self.entries {
207            if alternate {
208                map.entry(key, &format_args!("{:#}", value));
209            } else {
210                map.entry(key, &format_args!("{}", value));
211            }
212        }
213        map.finish()
214    }
215}
216
217/// Dynamically typed configuration value.
218#[derive(Debug, Clone, PartialEq)]
219pub enum Value {
220    Bool(bool),
221    Int(isize),
222    Float(f64),
223    String(String),
224    List(Vec<LocatedValue>),
225    Map(Map),
226    Null,
227}
228
229/// Comment text attached to a [`LocatedValue`]: lines preceding the key and an optional
230/// inline comment on the same line as the value.
231///
232/// Empty by default — callers check `.before().is_empty()` and `.after()` to detect absence.
233#[derive(Debug, Clone, PartialEq, Default)]
234pub struct Comment {
235    before: Vec<String>,
236    after: Option<String>,
237}
238
239impl Comment {
240    pub fn new() -> Self {
241        Self::default()
242    }
243
244    /// Comment lines preceding the key; empty when none.
245    pub fn before(&self) -> &[String] {
246        &self.before
247    }
248
249    pub fn before_mut(&mut self) -> &mut Vec<String> {
250        &mut self.before
251    }
252
253    /// Inline comment on the same line as the value; `None` when absent.
254    pub fn after(&self) -> Option<&str> {
255        self.after.as_deref()
256    }
257
258    pub fn after_mut(&mut self) -> &mut Option<String> {
259        &mut self.after
260    }
261
262    /// Builder: set the before-lines (replaces any existing).
263    pub fn with_before(mut self, lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
264        self.before = lines.into_iter().map(|l| l.into()).collect();
265        self
266    }
267
268    /// Builder: set the inline after-comment.
269    pub fn with_after(mut self, text: Option<impl Into<String>>) -> Self {
270        self.after = text.map(|t| t.into());
271        self
272    }
273
274    /// In-place setter for before-lines.
275    pub fn set_before(&mut self, lines: impl IntoIterator<Item = impl Into<String>>) {
276        self.before = lines.into_iter().map(|l| l.into()).collect();
277    }
278
279    /// In-place setter for the inline after-comment.
280    pub fn set_after(&mut self, text: Option<impl Into<String>>) {
281        self.after = text.map(|t| t.into());
282    }
283}
284
285/// A [`Value`] with its [`Location`] and an optional [`Comment`].
286///
287/// Fields are private; build with [`LocatedValue::new`] and access through the provided
288/// accessor methods. [`Display`] is compact by default; use `{value:#}` for a multiline dump
289/// with `@source:resource:line:column` on the first line.
290#[derive(Debug, Clone, PartialEq)]
291pub struct LocatedValue {
292    value: Value,
293    location: Location,
294    comment: Comment,
295}
296
297impl LocatedValue {
298    /// Create a located value with an empty (default) comment.
299    pub fn new(value: impl Into<Value>, location: impl Into<Location>) -> Self {
300        Self {
301            value: value.into(),
302            location: location.into(),
303            comment: Comment::new(),
304        }
305    }
306
307    // --- value ---
308
309    pub fn value(&self) -> &Value {
310        &self.value
311    }
312
313    pub fn value_mut(&mut self) -> &mut Value {
314        &mut self.value
315    }
316
317    pub fn into_value(self) -> Value {
318        self.value
319    }
320
321    pub fn with_value(mut self, value: impl Into<Value>) -> Self {
322        self.value = value.into();
323        self
324    }
325
326    pub fn set_value(&mut self, value: impl Into<Value>) {
327        self.value = value.into();
328    }
329
330    // --- location ---
331
332    pub fn location(&self) -> &Location {
333        &self.location
334    }
335
336    pub fn location_mut(&mut self) -> &mut Location {
337        &mut self.location
338    }
339
340    pub fn with_location(mut self, location: impl Into<Location>) -> Self {
341        self.location = location.into();
342        self
343    }
344
345    pub fn set_location(&mut self, location: impl Into<Location>) {
346        self.location = location.into();
347    }
348
349    // --- comment ---
350
351    pub fn comment(&self) -> &Comment {
352        &self.comment
353    }
354
355    pub fn comment_mut(&mut self) -> &mut Comment {
356        &mut self.comment
357    }
358
359    pub fn with_comment(mut self, comment: Comment) -> Self {
360        self.comment = comment;
361        self
362    }
363
364    pub fn set_comment(&mut self, comment: Comment) {
365        self.comment = comment;
366    }
367}
368
369impl Display for LocatedValue {
370    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
371        if f.alternate() {
372            let mut map = f.debug_map();
373            map.entry(&"value", &format_args!("{:#}", self.value));
374            map.entry(
375                &"location",
376                &format_args!("{:?}", self.location.to_string()),
377            );
378            if !self.comment.before.is_empty() || self.comment.after.is_some() {
379                map.entry(&"comment_before", &self.comment.before.as_slice());
380                if let Some(after) = &self.comment.after {
381                    map.entry(&"comment_after", &after.as_str());
382                }
383            }
384            map.finish()
385        } else {
386            write!(f, "{}", self.value)
387        }
388    }
389}
390
391impl AsRef<Value> for Value {
392    fn as_ref(&self) -> &Value {
393        self
394    }
395}
396
397impl AsRef<Value> for LocatedValue {
398    fn as_ref(&self) -> &Value {
399        &self.value
400    }
401}
402
403impl Value {
404    pub fn new_map() -> Self {
405        Self::Map(Map::new())
406    }
407
408    pub fn new_list() -> Self {
409        Self::List(Vec::new())
410    }
411
412    pub fn new_string() -> Self {
413        Self::String(String::new())
414    }
415
416    pub fn is_bool(&self) -> bool {
417        matches!(self, Self::Bool(_))
418    }
419
420    pub fn as_bool(&self) -> Option<bool> {
421        match self {
422            Self::Bool(value) => Some(*value),
423            _ => None,
424        }
425    }
426
427    pub fn into_bool(self) -> Option<bool> {
428        match self {
429            Self::Bool(value) => Some(value),
430            _ => None,
431        }
432    }
433
434    pub fn bool_mut(&mut self) -> Option<&mut bool> {
435        match self {
436            Self::Bool(value) => Some(value),
437            _ => None,
438        }
439    }
440
441    pub fn is_int(&self) -> bool {
442        matches!(self, Self::Int(_))
443    }
444
445    pub fn as_int(&self) -> Option<isize> {
446        match self {
447            Self::Int(value) => Some(*value),
448            _ => None,
449        }
450    }
451
452    pub fn into_int(self) -> Option<isize> {
453        match self {
454            Self::Int(value) => Some(value),
455            _ => None,
456        }
457    }
458
459    pub fn int_mut(&mut self) -> Option<&mut isize> {
460        match self {
461            Self::Int(value) => Some(value),
462            _ => None,
463        }
464    }
465
466    pub fn is_float(&self) -> bool {
467        matches!(self, Self::Float(_))
468    }
469
470    pub fn as_float(&self) -> Option<f64> {
471        match self {
472            Self::Float(value) => Some(*value),
473            _ => None,
474        }
475    }
476
477    pub fn into_float(self) -> Option<f64> {
478        match self {
479            Self::Float(value) => Some(value),
480            _ => None,
481        }
482    }
483
484    pub fn float_mut(&mut self) -> Option<&mut f64> {
485        match self {
486            Self::Float(value) => Some(value),
487            _ => None,
488        }
489    }
490
491    pub fn is_string(&self) -> bool {
492        matches!(self, Self::String(_))
493    }
494
495    pub fn as_string(&self) -> Option<&String> {
496        match self {
497            Self::String(value) => Some(value),
498            _ => None,
499        }
500    }
501
502    pub fn into_string(self) -> Option<String> {
503        match self {
504            Self::String(value) => Some(value),
505            _ => None,
506        }
507    }
508
509    pub fn string_mut(&mut self) -> Option<&mut String> {
510        match self {
511            Self::String(value) => Some(value),
512            _ => None,
513        }
514    }
515
516    pub fn is_list(&self) -> bool {
517        matches!(self, Self::List(_))
518    }
519
520    pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
521        match self {
522            Self::List(value) => Some(value),
523            _ => None,
524        }
525    }
526
527    pub fn into_list(self) -> Option<Vec<LocatedValue>> {
528        match self {
529            Self::List(value) => Some(value),
530            _ => None,
531        }
532    }
533
534    pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
535        match self {
536            Self::List(value) => Some(value),
537            _ => None,
538        }
539    }
540
541    pub fn is_map(&self) -> bool {
542        matches!(self, Self::Map(_))
543    }
544
545    pub fn as_map(&self) -> Option<&Map> {
546        match self {
547            Self::Map(value) => Some(value),
548            _ => None,
549        }
550    }
551
552    pub fn into_map(self) -> Option<Map> {
553        match self {
554            Self::Map(value) => Some(value),
555            _ => None,
556        }
557    }
558
559    pub fn map_mut(&mut self) -> Option<&mut Map> {
560        match self {
561            Self::Map(value) => Some(value),
562            _ => None,
563        }
564    }
565
566    pub fn is_null(&self) -> bool {
567        matches!(self, Self::Null)
568    }
569
570    pub fn type_name(&self) -> ValueType {
571        match self {
572            Self::Bool(_) => ValueType::Bool,
573            Self::Int(_) => ValueType::Int,
574            Self::Float(_) => ValueType::Float,
575            Self::String(_) => ValueType::String,
576            Self::List(_) => ValueType::List,
577            Self::Map(_) => ValueType::Map,
578            Self::Null => ValueType::Null,
579        }
580    }
581}
582
583impl Display for Value {
584    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
585        match self {
586            Self::Bool(value) => write!(f, "{value}"),
587            Self::Int(value) => write!(f, "{value}"),
588            Self::Float(value) => write!(f, "{value}"),
589            Self::String(value) => write!(f, "{value:?}"),
590            Self::List(values) => {
591                let alternate = f.alternate();
592                let mut list = f.debug_list();
593                for value in values {
594                    if alternate {
595                        list.entry(&format_args!("{:#}", value));
596                    } else {
597                        list.entry(&format_args!("{}", value));
598                    }
599                }
600                list.finish()
601            }
602            Self::Map(value) => Display::fmt(value, f),
603            Self::Null => f.write_str("null"),
604        }
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    fn located_string(text: &str) -> LocatedValue {
613        LocatedValue::new(
614            Value::String(text.to_string()),
615            Location::at("file", "test", None, None, None),
616        )
617    }
618
619    #[test]
620    fn as_ref_value_accepts_all_forms() {
621        fn take<V: AsRef<Value>>(value: V) -> Value {
622            value.as_ref().clone()
623        }
624        let value = Value::Int(7);
625        let located = LocatedValue::new(
626            Value::Int(7),
627            Location::at("file", "test", None, None, None),
628        );
629        assert_eq!(take(value.clone()), value);
630        assert_eq!(take(&value), value);
631        assert_eq!(take(located.clone()), value);
632        assert_eq!(take(&located), value);
633    }
634
635    #[test]
636    fn last_key_wins() {
637        let mut map = Map::new();
638        map.insert("foo".to_string(), located_string("first"));
639        map.insert("foo".to_string(), located_string("second"));
640        assert_eq!(
641            map.get("foo").unwrap().value().as_string().unwrap(),
642            "second"
643        );
644    }
645
646    #[test]
647    fn default_display_is_compact() {
648        let value = LocatedValue::new(
649            Value::String("hello".to_string()),
650            Location::at("file", "config.yaml", Some(2), Some(5), None),
651        );
652        let message = value.to_string();
653        assert!(!message.contains('\n'));
654        assert!(!message.starts_with('@'));
655        assert_eq!(message, "\"hello\"");
656    }
657
658    #[test]
659    fn alternate_display_shows_location_and_multiline() {
660        let value = LocatedValue::new(
661            Value::String("hello".to_string()),
662            Location::at("file", "config.yaml", Some(2), Some(5), None),
663        );
664        let message = format!("{value:#}");
665        assert_eq!(
666            message,
667            "{\n    \"value\": \"hello\",\n    \"location\": \"file:config.yaml:2:5\",\n}"
668        );
669        assert!(!message.contains('@'));
670    }
671
672    #[test]
673    fn value_accessors_and_constructors() {
674        let mut value = Value::Bool(true);
675        assert!(value.is_bool());
676        assert_eq!(value.as_bool(), Some(true));
677        assert_eq!(value.type_name(), ValueType::Bool);
678        if let Some(flag) = value.bool_mut() {
679            *flag = false;
680        }
681        assert_eq!(value.into_bool(), Some(false));
682
683        let list = Value::new_list();
684        assert!(list.is_list());
685        let map = Value::new_map();
686        assert!(map.is_map());
687        let text = Value::new_string();
688        assert!(text.is_string());
689    }
690
691    #[test]
692    fn map_remove_get_mut_and_display() {
693        let mut map = Map::new();
694        map.insert("a".to_string(), located_string("one"));
695        map.insert("b".to_string(), located_string("two"));
696        assert_eq!(map.len(), 2);
697        assert!(map.contains_key("a"));
698        assert!(map.get_mut("b").is_some());
699        let removed = map.remove("a");
700        assert!(removed.is_some());
701        assert!(!map.contains_key("a"));
702
703        let compact = format!("{map}");
704        assert!(compact.contains("b"));
705        let detailed = format!("{map:#}");
706        assert!(detailed.contains("location"));
707    }
708
709    #[test]
710    fn location_display_and_with_length() {
711        let location = Location::at("file", "", Some(1), Some(2), None).with_length(3);
712        assert_eq!(location.to_string(), "file:1:2");
713        let resourceful = Location::at("file", "cfg.yml", Some(4), None, None);
714        assert_eq!(resourceful.to_string(), "file:cfg.yml:4");
715    }
716
717    #[test]
718    fn comment_attached_to_located_value() {
719        let lv = LocatedValue::new(
720            Value::String("debug".into()),
721            Location::at("file", "baz.toml", Some(4), Some(9), None),
722        )
723        .with_comment(
724            Comment::new()
725                .with_before(["# log level: debug, info, warn, error"])
726                .with_after(Some("# inline")),
727        );
728        assert_eq!(
729            lv.comment().before(),
730            &["# log level: debug, info, warn, error"]
731        );
732        assert_eq!(lv.comment().after(), Some("# inline"));
733        assert_eq!(lv.value().as_string().unwrap(), "debug");
734    }
735
736    #[test]
737    fn comment_alternate_display_shows_comment_fields() {
738        let lv = LocatedValue::new(
739            Value::String("debug".into()),
740            Location::at("file", "baz.toml", Some(4), Some(9), None),
741        )
742        .with_comment(Comment::new().with_before(["# level comment"]));
743        let text = format!("{lv:#}");
744        assert!(text.contains("\"comment_before\""));
745        assert!(text.contains("level comment"));
746    }
747
748    #[test]
749    fn value_list_and_map_display_modes() {
750        let list = Value::List(vec![located_string("a"), located_string("b")]);
751        assert!(format!("{list}").contains("a"));
752        assert!(format!("{list:#}").contains("location"));
753
754        let mut map = Map::new();
755        map.insert("k".to_string(), located_string("v"));
756        let map_value = Value::Map(map);
757        assert!(format!("{map_value}").contains("k"));
758    }
759}