phie/
locator.rs

1// Copyright (c) 2022 Yegor Bugayenko
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included
11// in all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::loc::Loc;
22use lazy_static::lazy_static;
23use rstest::rstest;
24use std::fmt;
25use std::str::FromStr;
26
27/// Locator is a chain of attributes connected with dots,
28/// for example `𝜋.𝜋.𝛼0` is a locator.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Locator {
31    locs: Vec<Loc>,
32}
33
34/// Use this macro to create a locator faster:
35///
36/// ```
37/// use phie::ph;
38/// use phie::loc::Loc;
39/// use phie::locator::Locator;
40/// use std::str::FromStr;
41/// let k = ph!("𝜋.𝜋.𝛼0");
42/// ```
43#[macro_export]
44macro_rules! ph {
45    ($s:expr) => {
46        Locator::from_str($s).unwrap()
47    };
48}
49
50impl Locator {
51    /// Make a locator from a vector of attribute names:
52    ///
53    /// ```
54    /// use phie::loc::Loc;
55    /// use phie::locator::Locator;
56    /// let k = Locator::from_vec(vec![Loc::Phi, Loc::Delta]);
57    /// ```
58    pub fn from_vec(locs: Vec<Loc>) -> Locator {
59        Locator { locs }
60    }
61
62    /// Make a locator from a single attribute:
63    ///
64    /// ```
65    /// use phie::loc::Loc;
66    /// use phie::locator::Locator;
67    /// let k = Locator::from_loc(Loc::Phi);
68    /// ```
69    pub fn from_loc(loc: Loc) -> Locator {
70        Locator::from_vec(vec![loc])
71    }
72
73    /// Get a single attribute from the locator, by its position.
74    pub fn loc(&self, id: usize) -> Option<&Loc> {
75        self.locs.get(id)
76    }
77
78    /// Turn it into a vector of attributes.
79    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}