1use crate::loc::Loc;
22use lazy_static::lazy_static;
23use rstest::rstest;
24use std::fmt;
25use std::str::FromStr;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Locator {
31 locs: Vec<Loc>,
32}
33
34#[macro_export]
44macro_rules! ph {
45 ($s:expr) => {
46 Locator::from_str($s).unwrap()
47 };
48}
49
50impl Locator {
51 pub fn from_vec(locs: Vec<Loc>) -> Locator {
59 Locator { locs }
60 }
61
62 pub fn from_loc(loc: Loc) -> Locator {
70 Locator::from_vec(vec![loc])
71 }
72
73 pub fn loc(&self, id: usize) -> Option<&Loc> {
75 self.locs.get(id)
76 }
77
78 pub fn to_vec(&self) -> Vec<Loc> {
80 self.locs.clone()
81 }
82}
83
84type CheckFn = fn(&Locator) -> Option<String>;
85
86impl FromStr for Locator {
87 type Err = String;
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 lazy_static! {
90 static ref CHECKS: [CheckFn; 4] = [
91 |p: &Locator| -> Option<String> {
92 p.locs[1..]
93 .iter()
94 .find(|i| matches!(i, Loc::Obj(_)))
95 .map(|v| format!("{} can only stay at the first position", v))
96 },
97 |p: &Locator| {
98 p.locs[1..]
99 .iter()
100 .find(|i| matches!(i, Loc::Root))
101 .map(|v| format!("{} can only start a locator", v))
102 },
103 |p: &Locator| {
104 p.locs[0..1]
105 .iter()
106 .find(|i| matches!(i, Loc::Attr(_)))
107 .map(|v| format!("{} can't start a locator", v))
108 },
109 |p: &Locator| {
110 if matches!(p.locs[0], Loc::Obj(_)) && p.locs.len() > 1 {
111 Some(format!(
112 "{} can only be the first and only locator",
113 p.locs[0]
114 ))
115 } else {
116 None
117 }
118 },
119 ];
120 }
121 let p = Locator {
122 locs: s.split('.').map(|i| Loc::from_str(i).unwrap()).collect(),
123 };
124 for check in CHECKS.iter() {
125 if let Some(msg) = (check)(&p) {
126 return Err(format!("{} in '{}'", msg, p));
127 }
128 }
129 Ok(p)
130 }
131}
132
133impl fmt::Display for Locator {
134 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
135 f.write_str(
136 &self
137 .locs
138 .iter()
139 .map(|i| i.to_string())
140 .collect::<Vec<String>>()
141 .join("."),
142 )
143 }
144}
145
146#[rstest]
147#[case("Q")]
148#[case("&")]
149#[case("P")]
150#[case("^")]
151#[case("@")]
152#[case("ν78")]
153#[case("ρ.&.0.^.@.P.81")]
154#[case("Q.0.&.3.^")]
155#[case("𝜑.𝛼0.σ.𝛼3.ρ")]
156#[case("Φ.𝛼1")]
157#[case("𝜋.𝜋.𝛼9")]
158#[case("P.0")]
159#[case("P.0")]
160pub fn parses_and_prints(#[case] locator: String) {
161 let p1 = Locator::from_str(&locator).unwrap();
162 let p2 = Locator::from_str(&p1.to_string()).unwrap();
163 assert_eq!(p1, p2)
164}
165
166#[test]
167pub fn parses_and_prints_one() {
168 let locator = "ρ.&.0.^.^.@.P.81";
169 let p1 = Locator::from_str(locator).unwrap();
170 let p2 = Locator::from_str(&p1.to_string()).unwrap();
171 assert_eq!(p1, p2)
172}
173
174#[rstest]
175#[case("")]
176#[case("ν5.0.ν3")]
177#[case("𝜋.")]
178#[case(".ν5")]
179#[case("𝜋.ν5")]
180#[case("Q.Q")]
181#[case("5")]
182#[case("invalid syntax")]
183#[case("$ . 5")]
184#[should_panic]
185pub fn fails_on_incorrect_locator(#[case] locator: String) {
186 ph!(&locator);
187}
188
189#[rstest]
190#[case("P.0", 0, Loc::Pi)]
191pub fn fetches_loc_from_locator(
192 #[case] locator: String,
193 #[case] idx: usize,
194 #[case] expected: Loc,
195) {
196 assert_eq!(*ph!(&locator).loc(idx).unwrap(), expected);
197}