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    /// Pre-rendered `{error:#}` snippet: the ±3-line source window with gutter line numbers and a
20    /// `^^^` caret line under the offending span, already formatted at construction. Empty for
21    /// synthetic locations (no source text) or when there is no line to point at. Display just
22    /// prints this — it never re-computes it. Build it with [`Location::in_text`].
23    pub snippet: String,
24}
25
26/// Convert a 1-based `usize` position into the compact [`NonZeroU32`] storage.
27///
28/// Returns `None` for zero (treated as "no position") and clamps values larger
29/// than [`u32::MAX`] to `u32::MAX` rather than overflowing.
30fn position(value: usize) -> Option<NonZeroU32> {
31    NonZeroU32::new(u32::try_from(value).unwrap_or(u32::MAX))
32}
33
34impl Location {
35    /// Build a location from the full originating [`Source`], with no source snippet.
36    ///
37    /// Use [`Location::in_text`] instead when the source text is on hand, so `{error:#}` can render
38    /// a caret window; this constructor leaves [`snippet`](Self::snippet) empty.
39    pub fn in_source(
40        source: Source,
41        line: Option<usize>,
42        column: Option<usize>,
43        length: Option<usize>,
44    ) -> Self {
45        Self {
46            source,
47            line: line.and_then(position),
48            column: column.and_then(position),
49            length: length.and_then(position),
50            snippet: String::new(),
51        }
52    }
53
54    /// Build a location from the originating [`Source`] and its raw `text`, pre-rendering the
55    /// `{error:#}` [`snippet`](Self::snippet): the offending line plus three lines of context on
56    /// either side, each with a gutter line number, and a `^` caret line under the `column`/`length`
57    /// span. When `line` is `None` (e.g. single-line input with no position) the snippet is left
58    /// empty. The window is clamped to the file's bounds.
59    pub fn in_text(
60        source: Source,
61        text: &str,
62        line: Option<usize>,
63        column: Option<usize>,
64        length: Option<usize>,
65    ) -> Self {
66        let mut snippet = String::new();
67        if let Some(line_number) = line {
68            let highlight = length.unwrap_or(1).max(1);
69            let lines: Vec<&str> = text.split('\n').collect();
70            let offending = line_number.saturating_sub(1);
71            let start = offending.saturating_sub(3);
72            let end = (offending + 4).min(lines.len());
73            let gutter_width = end.to_string().len();
74            let mut rows: Vec<String> = Vec::new();
75            for (offset, line_text) in lines[start..end].iter().enumerate() {
76                let display_line = start + offset + 1;
77                let number = display_line.to_string();
78                let pad = gutter_width.saturating_sub(number.len());
79                let mut row = String::from("  ");
80                for _ in 0..pad {
81                    row.push(' ');
82                }
83                row.push_str(&number);
84                row.push_str(" | ");
85                row.push_str(line_text);
86                rows.push(row);
87                if display_line == line_number {
88                    let mut caret = String::from("  ");
89                    for _ in 0..pad + number.len() + 1 {
90                        caret.push(' ');
91                    }
92                    caret.push_str("| ");
93                    if let Some(column_number) = column {
94                        for _ in 1..column_number {
95                            caret.push(' ');
96                        }
97                    }
98                    for _ in 0..highlight {
99                        caret.push('^');
100                    }
101                    rows.push(caret);
102                }
103            }
104            snippet = rows.join("\n");
105        }
106        Self {
107            source,
108            line: line.and_then(position),
109            column: column.and_then(position),
110            length: length.and_then(position),
111            snippet,
112        }
113    }
114
115    /// Build a location from a bare source name and resource (a synthetic [`Source`] with no
116    /// options), for origins that do not come from parsing a real source string. No snippet.
117    pub fn at(
118        source_name: &str,
119        resource: &str,
120        line: Option<usize>,
121        column: Option<usize>,
122        length: Option<usize>,
123    ) -> Self {
124        Self::in_source(
125            Source::named(source_name).with_resource(resource),
126            line,
127            column,
128            length,
129        )
130    }
131
132    /// The originating source's name (loader kind).
133    pub fn source_name(&self) -> &str {
134        self.source.source()
135    }
136
137    /// The originating source's resource (address).
138    pub fn resource(&self) -> &str {
139        self.source.resource()
140    }
141
142    pub fn with_length(mut self, length: usize) -> Self {
143        self.length = position(length);
144        self
145    }
146}
147
148impl Display for Location {
149    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
150        let resource = self.source.resource();
151        if resource.is_empty() {
152            write!(f, "{}", self.source.source())?;
153        } else {
154            write!(f, "{}:{}", self.source.source(), resource)?;
155        }
156        match (self.line, self.column) {
157            (Some(line), Some(column)) => write!(f, ":{line}:{column}"),
158            (Some(line), None) => write!(f, ":{line}"),
159            _ => Ok(()),
160        }
161    }
162}
163
164/// Kind of value stored in [`Value`].
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub enum ValueType {
167    Bool,
168    Int,
169    Float,
170    String,
171    List,
172    Map,
173    Null,
174}
175
176impl Display for ValueType {
177    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
178        f.write_str(match self {
179            Self::Bool => "boolean",
180            Self::Int => "integer",
181            Self::Float => "float",
182            Self::String => "string",
183            Self::List => "list",
184            Self::Map => "map",
185            Self::Null => "null",
186        })
187    }
188}
189
190/// Ordered map of configuration keys to located values (last key wins on lookup).
191#[derive(Debug, Clone, PartialEq, Default)]
192pub struct Map {
193    entries: Vec<(String, LocatedValue)>,
194}
195
196impl Map {
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    pub fn len(&self) -> usize {
202        self.entries.len()
203    }
204
205    pub fn is_empty(&self) -> bool {
206        self.entries.is_empty()
207    }
208
209    pub fn contains_key(&self, key: &str) -> bool {
210        for index in (0..self.entries.len()).rev() {
211            if self.entries[index].0 == key {
212                return true;
213            }
214        }
215        false
216    }
217
218    pub fn get(&self, key: &str) -> Option<&LocatedValue> {
219        for index in (0..self.entries.len()).rev() {
220            if self.entries[index].0 == key {
221                return Some(&self.entries[index].1);
222            }
223        }
224        None
225    }
226
227    pub fn get_mut(&mut self, key: &str) -> Option<&mut LocatedValue> {
228        let mut found = None;
229        for index in (0..self.entries.len()).rev() {
230            if self.entries[index].0 == key {
231                found = Some(index);
232                break;
233            }
234        }
235        if let Some(index) = found {
236            Some(&mut self.entries[index].1)
237        } else {
238            None
239        }
240    }
241
242    pub fn insert(&mut self, key: String, value: LocatedValue) -> Option<LocatedValue> {
243        let old = self.remove(&key);
244        self.entries.push((key, value));
245        old
246    }
247
248    pub fn remove(&mut self, key: &str) -> Option<LocatedValue> {
249        let mut found = None;
250        for index in (0..self.entries.len()).rev() {
251            if self.entries[index].0 == key {
252                found = Some(index);
253                break;
254            }
255        }
256        if let Some(index) = found {
257            Some(self.entries.remove(index).1)
258        } else {
259            None
260        }
261    }
262
263    pub fn entries(&self) -> &[(String, LocatedValue)] {
264        &self.entries
265    }
266
267    pub fn entries_mut(&mut self) -> &mut Vec<(String, LocatedValue)> {
268        &mut self.entries
269    }
270}
271
272impl Display for Map {
273    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
274        let alternate = f.alternate();
275        let mut map = f.debug_map();
276        for (key, value) in &self.entries {
277            if alternate {
278                map.entry(key, &format_args!("{:#}", value));
279            } else {
280                map.entry(key, &format_args!("{}", value));
281            }
282        }
283        map.finish()
284    }
285}
286
287/// Dynamically typed configuration value.
288#[derive(Debug, Clone, PartialEq)]
289pub enum Value {
290    Bool(bool),
291    Int(isize),
292    Float(f64),
293    String(String),
294    List(Vec<LocatedValue>),
295    Map(Map),
296    Null,
297}
298
299/// Comment text attached to a [`LocatedValue`]: lines preceding the key and an optional
300/// inline comment on the same line as the value.
301///
302/// Empty by default — callers check `.before().is_empty()` and `.after()` to detect absence.
303#[derive(Debug, Clone, PartialEq, Default)]
304pub struct Comment {
305    before: Vec<String>,
306    after: Option<String>,
307}
308
309impl Comment {
310    pub fn new() -> Self {
311        Self::default()
312    }
313
314    /// Comment lines preceding the key; empty when none.
315    pub fn before(&self) -> &[String] {
316        &self.before
317    }
318
319    pub fn before_mut(&mut self) -> &mut Vec<String> {
320        &mut self.before
321    }
322
323    /// Inline comment on the same line as the value; `None` when absent.
324    pub fn after(&self) -> Option<&str> {
325        self.after.as_deref()
326    }
327
328    pub fn after_mut(&mut self) -> &mut Option<String> {
329        &mut self.after
330    }
331
332    /// Builder: set the before-lines (replaces any existing).
333    pub fn with_before(mut self, lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
334        self.before = lines.into_iter().map(|l| l.into()).collect();
335        self
336    }
337
338    /// Builder: set the inline after-comment.
339    pub fn with_after(mut self, text: Option<impl Into<String>>) -> Self {
340        self.after = text.map(|t| t.into());
341        self
342    }
343
344    /// In-place setter for before-lines.
345    pub fn set_before(&mut self, lines: impl IntoIterator<Item = impl Into<String>>) {
346        self.before = lines.into_iter().map(|l| l.into()).collect();
347    }
348
349    /// In-place setter for the inline after-comment.
350    pub fn set_after(&mut self, text: Option<impl Into<String>>) {
351        self.after = text.map(|t| t.into());
352    }
353}
354
355/// A [`Value`] with its [`Location`] and an optional [`Comment`].
356///
357/// Fields are private; build with [`LocatedValue::new`] and access through the provided
358/// accessor methods. [`Display`] is compact by default; use `{value:#}` for a multiline dump
359/// with `@source:resource:line:column` on the first line.
360#[derive(Debug, Clone, PartialEq)]
361pub struct LocatedValue {
362    value: Value,
363    location: Location,
364    comment: Comment,
365}
366
367impl LocatedValue {
368    /// Create a located value with an empty (default) comment.
369    pub fn new(value: impl Into<Value>, location: impl Into<Location>) -> Self {
370        Self {
371            value: value.into(),
372            location: location.into(),
373            comment: Comment::new(),
374        }
375    }
376
377    // --- value ---
378
379    pub fn value(&self) -> &Value {
380        &self.value
381    }
382
383    pub fn value_mut(&mut self) -> &mut Value {
384        &mut self.value
385    }
386
387    pub fn into_value(self) -> Value {
388        self.value
389    }
390
391    pub fn with_value(mut self, value: impl Into<Value>) -> Self {
392        self.value = value.into();
393        self
394    }
395
396    pub fn set_value(&mut self, value: impl Into<Value>) {
397        self.value = value.into();
398    }
399
400    // --- location ---
401
402    pub fn location(&self) -> &Location {
403        &self.location
404    }
405
406    pub fn location_mut(&mut self) -> &mut Location {
407        &mut self.location
408    }
409
410    pub fn with_location(mut self, location: impl Into<Location>) -> Self {
411        self.location = location.into();
412        self
413    }
414
415    pub fn set_location(&mut self, location: impl Into<Location>) {
416        self.location = location.into();
417    }
418
419    // --- comment ---
420
421    pub fn comment(&self) -> &Comment {
422        &self.comment
423    }
424
425    pub fn comment_mut(&mut self) -> &mut Comment {
426        &mut self.comment
427    }
428
429    pub fn with_comment(mut self, comment: Comment) -> Self {
430        self.comment = comment;
431        self
432    }
433
434    pub fn set_comment(&mut self, comment: Comment) {
435        self.comment = comment;
436    }
437}
438
439impl Display for LocatedValue {
440    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
441        if f.alternate() {
442            let mut map = f.debug_map();
443            map.entry(&"value", &format_args!("{:#}", self.value));
444            map.entry(
445                &"location",
446                &format_args!("{:?}", self.location.to_string()),
447            );
448            if !self.comment.before.is_empty() || self.comment.after.is_some() {
449                map.entry(&"comment_before", &self.comment.before.as_slice());
450                if let Some(after) = &self.comment.after {
451                    map.entry(&"comment_after", &after.as_str());
452                }
453            }
454            map.finish()
455        } else {
456            write!(f, "{}", self.value)
457        }
458    }
459}
460
461impl AsRef<Value> for Value {
462    fn as_ref(&self) -> &Value {
463        self
464    }
465}
466
467impl AsRef<Value> for LocatedValue {
468    fn as_ref(&self) -> &Value {
469        &self.value
470    }
471}
472
473impl Value {
474    pub fn new_map() -> Self {
475        Self::Map(Map::new())
476    }
477
478    pub fn new_list() -> Self {
479        Self::List(Vec::new())
480    }
481
482    pub fn new_string() -> Self {
483        Self::String(String::new())
484    }
485
486    pub fn is_bool(&self) -> bool {
487        matches!(self, Self::Bool(_))
488    }
489
490    pub fn as_bool(&self) -> Option<bool> {
491        match self {
492            Self::Bool(value) => Some(*value),
493            _ => None,
494        }
495    }
496
497    pub fn into_bool(self) -> Option<bool> {
498        match self {
499            Self::Bool(value) => Some(value),
500            _ => None,
501        }
502    }
503
504    pub fn bool_mut(&mut self) -> Option<&mut bool> {
505        match self {
506            Self::Bool(value) => Some(value),
507            _ => None,
508        }
509    }
510
511    pub fn is_int(&self) -> bool {
512        matches!(self, Self::Int(_))
513    }
514
515    pub fn as_int(&self) -> Option<isize> {
516        match self {
517            Self::Int(value) => Some(*value),
518            _ => None,
519        }
520    }
521
522    pub fn into_int(self) -> Option<isize> {
523        match self {
524            Self::Int(value) => Some(value),
525            _ => None,
526        }
527    }
528
529    pub fn int_mut(&mut self) -> Option<&mut isize> {
530        match self {
531            Self::Int(value) => Some(value),
532            _ => None,
533        }
534    }
535
536    pub fn is_float(&self) -> bool {
537        matches!(self, Self::Float(_))
538    }
539
540    pub fn as_float(&self) -> Option<f64> {
541        match self {
542            Self::Float(value) => Some(*value),
543            _ => None,
544        }
545    }
546
547    pub fn into_float(self) -> Option<f64> {
548        match self {
549            Self::Float(value) => Some(value),
550            _ => None,
551        }
552    }
553
554    pub fn float_mut(&mut self) -> Option<&mut f64> {
555        match self {
556            Self::Float(value) => Some(value),
557            _ => None,
558        }
559    }
560
561    pub fn is_string(&self) -> bool {
562        matches!(self, Self::String(_))
563    }
564
565    pub fn as_string(&self) -> Option<&String> {
566        match self {
567            Self::String(value) => Some(value),
568            _ => None,
569        }
570    }
571
572    pub fn into_string(self) -> Option<String> {
573        match self {
574            Self::String(value) => Some(value),
575            _ => None,
576        }
577    }
578
579    pub fn string_mut(&mut self) -> Option<&mut String> {
580        match self {
581            Self::String(value) => Some(value),
582            _ => None,
583        }
584    }
585
586    pub fn is_list(&self) -> bool {
587        matches!(self, Self::List(_))
588    }
589
590    pub fn as_list(&self) -> Option<&Vec<LocatedValue>> {
591        match self {
592            Self::List(value) => Some(value),
593            _ => None,
594        }
595    }
596
597    pub fn into_list(self) -> Option<Vec<LocatedValue>> {
598        match self {
599            Self::List(value) => Some(value),
600            _ => None,
601        }
602    }
603
604    pub fn list_mut(&mut self) -> Option<&mut Vec<LocatedValue>> {
605        match self {
606            Self::List(value) => Some(value),
607            _ => None,
608        }
609    }
610
611    pub fn is_map(&self) -> bool {
612        matches!(self, Self::Map(_))
613    }
614
615    pub fn as_map(&self) -> Option<&Map> {
616        match self {
617            Self::Map(value) => Some(value),
618            _ => None,
619        }
620    }
621
622    pub fn into_map(self) -> Option<Map> {
623        match self {
624            Self::Map(value) => Some(value),
625            _ => None,
626        }
627    }
628
629    pub fn map_mut(&mut self) -> Option<&mut Map> {
630        match self {
631            Self::Map(value) => Some(value),
632            _ => None,
633        }
634    }
635
636    pub fn is_null(&self) -> bool {
637        matches!(self, Self::Null)
638    }
639
640    pub fn type_name(&self) -> ValueType {
641        match self {
642            Self::Bool(_) => ValueType::Bool,
643            Self::Int(_) => ValueType::Int,
644            Self::Float(_) => ValueType::Float,
645            Self::String(_) => ValueType::String,
646            Self::List(_) => ValueType::List,
647            Self::Map(_) => ValueType::Map,
648            Self::Null => ValueType::Null,
649        }
650    }
651}
652
653impl Display for Value {
654    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
655        match self {
656            Self::Bool(value) => write!(f, "{value}"),
657            Self::Int(value) => write!(f, "{value}"),
658            Self::Float(value) => write!(f, "{value}"),
659            Self::String(value) => write!(f, "{value:?}"),
660            Self::List(values) => {
661                let alternate = f.alternate();
662                let mut list = f.debug_list();
663                for value in values {
664                    if alternate {
665                        list.entry(&format_args!("{:#}", value));
666                    } else {
667                        list.entry(&format_args!("{}", value));
668                    }
669                }
670                list.finish()
671            }
672            Self::Map(value) => Display::fmt(value, f),
673            Self::Null => f.write_str("null"),
674        }
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681
682    fn located_string(text: &str) -> LocatedValue {
683        LocatedValue::new(
684            Value::String(text.to_string()),
685            Location::at("file", "test", None, None, None),
686        )
687    }
688
689    #[test]
690    fn as_ref_value_accepts_all_forms() {
691        fn take<V: AsRef<Value>>(value: V) -> Value {
692            value.as_ref().clone()
693        }
694        let value = Value::Int(7);
695        let located = LocatedValue::new(
696            Value::Int(7),
697            Location::at("file", "test", None, None, None),
698        );
699        assert_eq!(take(value.clone()), value);
700        assert_eq!(take(&value), value);
701        assert_eq!(take(located.clone()), value);
702        assert_eq!(take(&located), value);
703    }
704
705    #[test]
706    fn last_key_wins() {
707        let mut map = Map::new();
708        map.insert("foo".to_string(), located_string("first"));
709        map.insert("foo".to_string(), located_string("second"));
710        assert_eq!(
711            map.get("foo").unwrap().value().as_string().unwrap(),
712            "second"
713        );
714    }
715
716    #[test]
717    fn default_display_is_compact() {
718        let value = LocatedValue::new(
719            Value::String("hello".to_string()),
720            Location::at("file", "config.yaml", Some(2), Some(5), None),
721        );
722        let message = value.to_string();
723        assert!(!message.contains('\n'));
724        assert!(!message.starts_with('@'));
725        assert_eq!(message, "\"hello\"");
726    }
727
728    #[test]
729    fn alternate_display_shows_location_and_multiline() {
730        let value = LocatedValue::new(
731            Value::String("hello".to_string()),
732            Location::at("file", "config.yaml", Some(2), Some(5), None),
733        );
734        let message = format!("{value:#}");
735        assert_eq!(
736            message,
737            "{\n    \"value\": \"hello\",\n    \"location\": \"file:config.yaml:2:5\",\n}"
738        );
739        assert!(!message.contains('@'));
740    }
741
742    #[test]
743    fn value_accessors_and_constructors() {
744        let mut value = Value::Bool(true);
745        assert!(value.is_bool());
746        assert_eq!(value.as_bool(), Some(true));
747        assert_eq!(value.type_name(), ValueType::Bool);
748        if let Some(flag) = value.bool_mut() {
749            *flag = false;
750        }
751        assert_eq!(value.into_bool(), Some(false));
752
753        let list = Value::new_list();
754        assert!(list.is_list());
755        let map = Value::new_map();
756        assert!(map.is_map());
757        let text = Value::new_string();
758        assert!(text.is_string());
759    }
760
761    #[test]
762    fn map_remove_get_mut_and_display() {
763        let mut map = Map::new();
764        map.insert("a".to_string(), located_string("one"));
765        map.insert("b".to_string(), located_string("two"));
766        assert_eq!(map.len(), 2);
767        assert!(map.contains_key("a"));
768        assert!(map.get_mut("b").is_some());
769        let removed = map.remove("a");
770        assert!(removed.is_some());
771        assert!(!map.contains_key("a"));
772
773        let compact = format!("{map}");
774        assert!(compact.contains("b"));
775        let detailed = format!("{map:#}");
776        assert!(detailed.contains("location"));
777    }
778
779    #[test]
780    fn location_display_and_with_length() {
781        let location = Location::at("file", "", Some(1), Some(2), None).with_length(3);
782        assert_eq!(location.to_string(), "file:1:2");
783        let resourceful = Location::at("file", "cfg.yml", Some(4), None, None);
784        assert_eq!(resourceful.to_string(), "file:cfg.yml:4");
785    }
786
787    #[test]
788    fn in_text_renders_gutter_and_caret_window() {
789        let text = "a\nb\nc\ntarget\ne\nf\ng\n";
790        // Offending token on line 4, column 1, three characters wide.
791        let location = Location::in_source(Source::named("file"), None, None, None);
792        let with_snippet =
793            Location::in_text(location.source.clone(), text, Some(4), Some(1), Some(3));
794        let snippet = &with_snippet.snippet;
795        // Three lines of context on each side are included (lines 1..=7).
796        for number in 1..=7 {
797            assert!(
798                snippet.contains(&format!("{number} | ")),
799                "expected gutter for line {number} in:\n{snippet}"
800            );
801        }
802        // The caret line underlines the offending span with `length` carets.
803        assert!(snippet.contains("^^^"), "expected caret in:\n{snippet}");
804        let target_line = snippet
805            .lines()
806            .find(|line| line.contains("target"))
807            .expect("target line");
808        let caret_line = snippet
809            .lines()
810            .find(|line| line.contains('^'))
811            .expect("caret line");
812        assert_eq!(
813            target_line.find('|'),
814            caret_line.find('|'),
815            "gutter pipes should align:\n{snippet}"
816        );
817    }
818
819    #[test]
820    fn in_text_clamps_window_near_bounds() {
821        let text = "only\nline\nhere\n";
822        let location = Location::in_text(Source::named("file"), text, Some(1), Some(1), None);
823        // The window cannot extend before the first line; single caret when no length.
824        assert!(location.snippet.contains("1 | only"));
825        assert!(location.snippet.contains('^'));
826        assert!(!location.snippet.contains("0 | "));
827    }
828
829    #[test]
830    fn in_text_without_line_leaves_snippet_empty() {
831        let location = Location::in_text(Source::named("file"), "a\nb\n", None, None, None);
832        assert!(location.snippet.is_empty());
833    }
834
835    #[test]
836    fn in_source_and_at_leave_snippet_empty() {
837        assert!(
838            Location::in_source(Source::named("file"), Some(2), Some(1), None)
839                .snippet
840                .is_empty()
841        );
842        assert!(
843            Location::at("file", "cfg", Some(2), Some(1), None)
844                .snippet
845                .is_empty()
846        );
847    }
848
849    #[test]
850    fn comment_attached_to_located_value() {
851        let lv = LocatedValue::new(
852            Value::String("debug".into()),
853            Location::at("file", "baz.toml", Some(4), Some(9), None),
854        )
855        .with_comment(
856            Comment::new()
857                .with_before(["# log level: debug, info, warn, error"])
858                .with_after(Some("# inline")),
859        );
860        assert_eq!(
861            lv.comment().before(),
862            &["# log level: debug, info, warn, error"]
863        );
864        assert_eq!(lv.comment().after(), Some("# inline"));
865        assert_eq!(lv.value().as_string().unwrap(), "debug");
866    }
867
868    #[test]
869    fn comment_alternate_display_shows_comment_fields() {
870        let lv = LocatedValue::new(
871            Value::String("debug".into()),
872            Location::at("file", "baz.toml", Some(4), Some(9), None),
873        )
874        .with_comment(Comment::new().with_before(["# level comment"]));
875        let text = format!("{lv:#}");
876        assert!(text.contains("\"comment_before\""));
877        assert!(text.contains("level comment"));
878    }
879
880    #[test]
881    fn value_list_and_map_display_modes() {
882        let list = Value::List(vec![located_string("a"), located_string("b")]);
883        assert!(format!("{list}").contains("a"));
884        assert!(format!("{list:#}").contains("location"));
885
886        let mut map = Map::new();
887        map.insert("k".to_string(), located_string("v"));
888        let map_value = Value::Map(map);
889        assert!(format!("{map_value}").contains("k"));
890    }
891}