1use std::fmt;
2
3#[derive(Debug, Clone, Default, PartialEq, Eq)]
4pub struct InputPath {
5 maybe_filename: Option<String>,
6 maybe_line_number: Option<usize>,
7 segment_list: Vec<PathSegment>,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum PathSegment {
12 Key(String),
13 Index(usize),
14}
15
16impl InputPath {
17 pub fn root() -> Self {
18 Self {
19 maybe_filename: None,
20 maybe_line_number: None,
21 segment_list: Vec::with_capacity(4),
22 }
23 }
24
25 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
26 self.maybe_filename = Some(filename.into());
27 self
28 }
29
30 pub fn with_line_number(mut self, line_number: usize) -> Self {
31 self.maybe_line_number = Some(line_number);
32 self
33 }
34
35 pub fn with_key(mut self, key: impl Into<String>) -> Self {
36 self.segment_list.push(PathSegment::Key(key.into()));
37 self
38 }
39
40 pub fn with_index(mut self, index: usize) -> Self {
41 self.segment_list.push(PathSegment::Index(index));
42 self
43 }
44
45 pub fn is_empty(&self) -> bool {
46 self.maybe_filename.is_none()
47 && self.maybe_line_number.is_none()
48 && self.segment_list.is_empty()
49 }
50}
51
52impl fmt::Display for InputPath {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 let mut has_path = false;
55 if let Some(filename) = &self.maybe_filename {
56 write!(f, "{filename}")?;
57 has_path = true;
58 }
59 let mut has_line_number = false;
60 if let Some(line_number) = &self.maybe_line_number {
61 if has_path {
62 write!(f, ":{line_number}")?;
63 } else {
64 write!(f, "{line_number}")?;
65 }
66 has_line_number = true;
67 }
68 if has_line_number && !self.segment_list.is_empty() {
69 write!(f, ":")?;
70 }
71 for segment in &self.segment_list {
72 match segment {
73 PathSegment::Key(key) => write!(f, "[{key}]")?,
74 PathSegment::Index(index) => write!(f, "[{index}]")?,
75 }
76 }
77 Ok(())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::InputPath;
84
85 #[test]
86 fn root_is_empty() {
87 assert!(InputPath::root().is_empty());
88 assert_eq!(InputPath::root().to_string(), "");
89 }
90
91 #[test]
92 fn segment_path_display() {
93 let path = InputPath::root()
94 .with_key("foo")
95 .with_index(0)
96 .with_key("bar");
97 assert!(!path.is_empty());
98 assert_eq!(path.to_string(), "[foo][0][bar]");
99 }
100
101 #[test]
102 fn filename_display() {
103 let path = InputPath::root().with_filename("config.toml");
104 assert!(!path.is_empty());
105 assert_eq!(path.to_string(), "config.toml");
106 }
107
108 #[test]
109 fn line_number_without_filename() {
110 let path = InputPath::root().with_line_number(12);
111 assert!(!path.is_empty());
112 assert_eq!(path.to_string(), "12");
113 }
114
115 #[test]
116 fn filename_and_line_number() {
117 let path = InputPath::root()
118 .with_filename("config.toml")
119 .with_line_number(42);
120 assert_eq!(path.to_string(), "config.toml:42");
121 }
122
123 #[test]
124 fn line_number_and_segments_use_colon_separator() {
125 let path = InputPath::root()
126 .with_line_number(3)
127 .with_key("items")
128 .with_index(1);
129 assert_eq!(path.to_string(), "3:[items][1]");
130 }
131
132 #[test]
133 fn filename_line_number_and_segments() {
134 let path = InputPath::root()
135 .with_filename("app.json")
136 .with_line_number(7)
137 .with_key("server")
138 .with_key("host");
139 assert_eq!(path.to_string(), "app.json:7:[server][host]");
140 }
141
142 #[test]
143 fn builders_do_not_mutate_cloned_path() {
144 let base = InputPath::root().with_key("root");
145 let child = base.clone().with_key("child");
146 assert_eq!(base.to_string(), "[root]");
147 assert_eq!(child.to_string(), "[root][child]");
148 }
149}