1use std::fmt::{Debug, Display, Formatter};
2use std::num::NonZeroU32;
3use tanzim_source::Source;
4
5#[derive(Debug, Clone, PartialEq)]
13pub struct Location {
14 pub source: Source,
15 pub line: Option<NonZeroU32>,
16 pub column: Option<NonZeroU32>,
17 pub length: Option<NonZeroU32>,
19 pub snippet: String,
24}
25
26fn position(value: usize) -> Option<NonZeroU32> {
31 NonZeroU32::new(u32::try_from(value).unwrap_or(u32::MAX))
32}
33
34impl Location {
35 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 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 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 pub fn source_name(&self) -> &str {
134 self.source.source()
135 }
136
137 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#[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#[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#[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#[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 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 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 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 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 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 pub fn set_after(&mut self, text: Option<impl Into<String>>) {
351 self.after = text.map(|t| t.into());
352 }
353}
354
355#[derive(Debug, Clone, PartialEq)]
361pub struct LocatedValue {
362 value: Value,
363 location: Location,
364 comment: Comment,
365}
366
367impl LocatedValue {
368 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 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 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 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 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 for number in 1..=7 {
797 assert!(
798 snippet.contains(&format!("{number} | ")),
799 "expected gutter for line {number} in:\n{snippet}"
800 );
801 }
802 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 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}