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