Skip to main content

plugx_input/
position.rs

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}