Skip to main content

olai_store/
name.rs

1use crate::{Error, Result};
2
3use std::borrow::Borrow;
4use std::fmt::{Display, Formatter};
5use std::hash::{Hash, Hasher};
6use std::iter::Peekable;
7use std::ops::Deref;
8use std::sync::LazyLock;
9
10use serde::{Deserialize, Serialize};
11
12pub static EMPTY_RESOURCE_NAME: LazyLock<ResourceName> =
13    LazyLock::new(|| ResourceName::new(&[] as &[String]));
14
15/// A (possibly nested) resource name represented as a path of field names.
16#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
17#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
18#[cfg_attr(feature = "sqlx", sqlx(transparent, no_pg_array))]
19pub struct ResourceName(Vec<String>);
20
21impl ResourceName {
22    /// Creates a new resource name from an iterator of field names.
23    pub fn new<A>(iter: impl IntoIterator<Item = A>) -> Self
24    where
25        Self: FromIterator<A>,
26    {
27        iter.into_iter().collect()
28    }
29
30    /// Naively splits a string at dots to create a resource name.
31    ///
32    /// This method is _NOT_ recommended for production use, as it does not attempt to interpret
33    /// special characters in field names.
34    pub fn from_naive_str_split(name: impl AsRef<str>) -> Self {
35        Self::new(name.as_ref().split(FIELD_SEPARATOR))
36    }
37
38    /// Parses a comma-separated list of resource names, properly accounting for escapes and special
39    /// characters.
40    pub fn parse_column_name_list(names: impl AsRef<str>) -> Result<Vec<ResourceName>> {
41        let names = names.as_ref();
42        let chars = &mut names.chars().peekable();
43
44        drop_leading_whitespace(chars);
45        let mut ending = match chars.peek() {
46            Some(_) => FieldEnding::NextColumn,
47            None => FieldEnding::InputExhausted,
48        };
49
50        let mut cols = vec![];
51        while ending == FieldEnding::NextColumn {
52            let (col, new_ending) = parse_resource_name(chars)?;
53            cols.push(col);
54            ending = new_ending;
55        }
56        Ok(cols)
57    }
58
59    /// Joins this name with another, concatenating their fields into a single nested path.
60    pub fn join(&self, right: &ResourceName) -> ResourceName {
61        [self.clone(), right.clone()].into_iter().collect()
62    }
63
64    /// The path of field names for this resource name.
65    pub fn path(&self) -> &[String] {
66        &self.0
67    }
68
69    /// Consumes this resource name and returns the path of field names.
70    pub fn into_inner(self) -> Vec<String> {
71        self.0
72    }
73
74    /// Returns true if this name starts with the given prefix.
75    pub fn prefix_matches(&self, prefix: &ResourceName) -> bool {
76        if self.len() < prefix.len() {
77            return false;
78        }
79        prefix.iter().zip(self.iter()).all(|(a, b)| a == b)
80    }
81}
82
83impl<A: Into<String>> FromIterator<A> for ResourceName {
84    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
85        let path = iter.into_iter().map(|s| s.into()).collect();
86        Self(path)
87    }
88}
89
90impl From<Vec<String>> for ResourceName {
91    fn from(path: Vec<String>) -> Self {
92        Self(path)
93    }
94}
95
96impl FromIterator<ResourceName> for ResourceName {
97    fn from_iter<T: IntoIterator<Item = ResourceName>>(iter: T) -> Self {
98        let path = iter.into_iter().flat_map(|c| c.into_iter()).collect();
99        Self(path)
100    }
101}
102
103impl IntoIterator for ResourceName {
104    type Item = String;
105    type IntoIter = std::vec::IntoIter<Self::Item>;
106
107    fn into_iter(self) -> Self::IntoIter {
108        self.0.into_iter()
109    }
110}
111
112impl Deref for ResourceName {
113    type Target = [String];
114
115    fn deref(&self) -> &[String] {
116        &self.0
117    }
118}
119
120impl Borrow<[String]> for ResourceName {
121    fn borrow(&self) -> &[String] {
122        self
123    }
124}
125
126impl Borrow<[String]> for &ResourceName {
127    fn borrow(&self) -> &[String] {
128        self
129    }
130}
131
132impl Hash for ResourceName {
133    fn hash<H: Hasher>(&self, hasher: &mut H) {
134        (**self).hash(hasher)
135    }
136}
137
138impl Display for ResourceName {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        for (i, s) in self.iter().enumerate() {
141            use std::fmt::Write as _;
142
143            if i > 0 {
144                f.write_char(FIELD_SEPARATOR)?;
145            }
146
147            let digit_char = |c: char| c.is_ascii_digit();
148            if s.is_empty() || s.starts_with(digit_char) || s.contains(|c| !is_simple_char(c)) {
149                f.write_char(FIELD_ESCAPE_CHAR)?;
150                for c in s.chars() {
151                    f.write_char(c)?;
152                    if c == FIELD_ESCAPE_CHAR {
153                        f.write_char(c)?;
154                    }
155                }
156                f.write_char(FIELD_ESCAPE_CHAR)?;
157            } else {
158                f.write_str(s)?;
159            }
160        }
161        Ok(())
162    }
163}
164
165fn is_simple_char(c: char) -> bool {
166    c.is_ascii_alphanumeric() || c == '_'
167}
168
169fn drop_leading_whitespace(iter: &mut Peekable<impl Iterator<Item = char>>) {
170    while iter.next_if(|c| c.is_whitespace()).is_some() {}
171}
172
173impl std::str::FromStr for ResourceName {
174    type Err = Error;
175
176    fn from_str(s: &str) -> Result<Self> {
177        match parse_resource_name(&mut s.chars().peekable())? {
178            (_, FieldEnding::NextColumn) => Err(Error::generic("Trailing comma in column name")),
179            (col, _) => Ok(col),
180        }
181    }
182}
183
184type Chars<'a> = Peekable<std::str::Chars<'a>>;
185
186#[derive(PartialEq)]
187enum FieldEnding {
188    InputExhausted,
189    NextField,
190    NextColumn,
191}
192
193const FIELD_ESCAPE_CHAR: char = '`';
194const FIELD_SEPARATOR: char = '.';
195const COLUMN_SEPARATOR: char = ',';
196
197fn parse_resource_name(chars: &mut Chars<'_>) -> Result<(ResourceName, FieldEnding)> {
198    drop_leading_whitespace(chars);
199    let mut ending = if chars.peek().is_none() {
200        FieldEnding::InputExhausted
201    } else if chars.next_if_eq(&COLUMN_SEPARATOR).is_some() {
202        FieldEnding::NextColumn
203    } else {
204        FieldEnding::NextField
205    };
206
207    let mut path = vec![];
208    while ending == FieldEnding::NextField {
209        drop_leading_whitespace(chars);
210        let field_name = match chars.next_if_eq(&FIELD_ESCAPE_CHAR) {
211            Some(_) => parse_escaped_field_name(chars)?,
212            None => parse_simple_field_name(chars)?,
213        };
214
215        ending = match chars.find(|c| !c.is_whitespace()) {
216            None => FieldEnding::InputExhausted,
217            Some(FIELD_SEPARATOR) => FieldEnding::NextField,
218            Some(COLUMN_SEPARATOR) => FieldEnding::NextColumn,
219            Some(other) => {
220                return Err(Error::generic(format!(
221                    "Invalid character {other:?} after field {field_name:?}",
222                )));
223            }
224        };
225        path.push(field_name);
226    }
227    Ok((ResourceName::new(path), ending))
228}
229
230fn parse_simple_field_name(chars: &mut Chars<'_>) -> Result<String> {
231    let mut name = String::new();
232    let mut first = true;
233    while let Some(c) = chars.next_if(|c| is_simple_char(*c)) {
234        if first && c.is_ascii_digit() {
235            return Err(Error::generic(format!(
236                "Unescaped field name cannot start with a digit {c:?}"
237            )));
238        }
239        name.push(c);
240        first = false;
241    }
242    Ok(name)
243}
244
245fn parse_escaped_field_name(chars: &mut Chars<'_>) -> Result<String> {
246    let mut name = String::new();
247    loop {
248        match chars.next() {
249            Some(FIELD_ESCAPE_CHAR) if chars.next_if_eq(&FIELD_ESCAPE_CHAR).is_none() => break,
250            Some(c) => name.push(c),
251            None => {
252                return Err(Error::generic(format!(
253                    "No closing {FIELD_ESCAPE_CHAR:?} after field {name:?}"
254                )));
255            }
256        }
257    }
258    Ok(name)
259}
260
261#[cfg(test)]
262mod test {
263    use super::*;
264
265    impl ResourceName {
266        fn empty() -> Self {
267            Self::new(&[] as &[String])
268        }
269    }
270
271    #[test]
272    fn test_column_name_methods() {
273        let simple: ResourceName = "x".parse().unwrap();
274        let nested: ResourceName = "x.y".parse().unwrap();
275
276        assert_eq!(simple.path(), ["x"]);
277        assert_eq!(nested.path(), ["x", "y"]);
278
279        assert_eq!(simple.clone().into_inner(), ["x"]);
280        assert_eq!(nested.clone().into_inner(), ["x", "y"]);
281
282        let name: &[String] = &nested;
283        assert_eq!(name, &["x", "y"]);
284
285        let name: ResourceName = ["x", "y"].into_iter().collect();
286        assert_eq!(name, nested);
287
288        let name: ResourceName = [&nested, &simple].into_iter().cloned().collect();
289        assert_eq!(name, ResourceName::new(["x", "y", "x"]));
290    }
291
292    #[test]
293    fn test_column_name_from_str() {
294        let cases = [
295            ("", Some(ResourceName::empty())),
296            (".", Some(ResourceName::new(["", ""]))),
297            ("  .  ", Some(ResourceName::new(["", ""]))),
298            (" ", Some(ResourceName::empty())),
299            ("0", None),
300            (".a", Some(ResourceName::new(["", "a"]))),
301            ("a.", Some(ResourceName::new(["a", ""]))),
302            ("  a  .  ", Some(ResourceName::new(["a", ""]))),
303            ("a..b", Some(ResourceName::new(["a", "", "b"]))),
304            ("`a", None),
305            ("a`", None),
306            ("a`b`", None),
307            ("`a`b", None),
308            ("`a``b`", Some(ResourceName::new(["a`b"]))),
309            ("  `a``b`  ", Some(ResourceName::new(["a`b"]))),
310            ("`a`` b`", Some(ResourceName::new(["a` b"]))),
311            ("a", Some(ResourceName::new(["a"]))),
312            ("a0", Some(ResourceName::new(["a0"]))),
313            ("`a`", Some(ResourceName::new(["a"]))),
314            ("  `a`  ", Some(ResourceName::new(["a"]))),
315            ("` `", Some(ResourceName::new([" "]))),
316            ("  ` `  ", Some(ResourceName::new([" "]))),
317            ("`0`", Some(ResourceName::new(["0"]))),
318            ("`.`", Some(ResourceName::new(["."]))),
319            ("`.`.`.`", Some(ResourceName::new([".", "."]))),
320            ("` `.` `", Some(ResourceName::new([" ", " "]))),
321            ("a.b", Some(ResourceName::new(["a", "b"]))),
322            ("a b", None),
323            ("a.`b`", Some(ResourceName::new(["a", "b"]))),
324            ("`a`.b", Some(ResourceName::new(["a", "b"]))),
325            ("`a`.`b`", Some(ResourceName::new(["a", "b"]))),
326            ("`a`.`b`.`c`", Some(ResourceName::new(["a", "b", "c"]))),
327            ("`a``.`b```", None),
328            ("`a```.`b``", None),
329            ("`a```.`b```", Some(ResourceName::new(["a`", "b`"]))),
330            ("`a.`b``.c`", None),
331            ("`a.``b`.c`", None),
332            ("`a.``b``.c`", Some(ResourceName::new(["a.`b`.c"]))),
333            ("a`.b``", None),
334        ];
335        for (input, expected_output) in cases {
336            let output: Result<ResourceName> = input.parse();
337            match (&output, &expected_output) {
338                (Ok(output), Some(expected_output)) => {
339                    assert_eq!(output, expected_output, "from {input}")
340                }
341                (Err(_), None) => {}
342                _ => panic!("Expected {input} to parse as {expected_output:?}, got {output:?}"),
343            }
344        }
345    }
346
347    #[test]
348    fn test_column_name_to_string() {
349        let cases = [
350            ("", ResourceName::empty()),
351            ("``.``", ResourceName::new(["", ""])),
352            ("``.a", ResourceName::new(["", "a"])),
353            ("a.``", ResourceName::new(["a", ""])),
354            ("a.``.b", ResourceName::new(["a", "", "b"])),
355            ("a", ResourceName::new(["a"])),
356            ("a0", ResourceName::new(["a0"])),
357            ("`a `", ResourceName::new(["a "])),
358            ("` `", ResourceName::new([" "])),
359            ("`0`", ResourceName::new(["0"])),
360            ("`.`", ResourceName::new(["."])),
361            ("`.`.`.`", ResourceName::new([".", "."])),
362            ("` `.` `", ResourceName::new([" ", " "])),
363            ("a.b", ResourceName::new(["a", "b"])),
364            ("a.b.c", ResourceName::new(["a", "b", "c"])),
365            ("a.`b.c`.d", ResourceName::new(["a", "b.c", "d"])),
366            ("`a```.`b```", ResourceName::new(["a`", "b`"])),
367        ];
368        for (expected_output, input) in cases {
369            let output = input.to_string();
370            assert_eq!(output, expected_output);
371
372            let parsed: ResourceName = output.parse().expect(&output);
373            assert_eq!(parsed, input);
374        }
375
376        let cases = [
377            ("  `a`  ", "a", ResourceName::new(["a"])),
378            ("  `a0`  ", "a0", ResourceName::new(["a0"])),
379            ("  `a`  .  `b`  ", "a.b", ResourceName::new(["a", "b"])),
380        ];
381        for (input, expected_output, expected_parsed) in cases {
382            let parsed: ResourceName = input.parse().unwrap();
383            assert_eq!(parsed, expected_parsed);
384            assert_eq!(parsed.to_string(), expected_output);
385        }
386    }
387
388    #[test]
389    fn test_parse_column_name_list() {
390        let cases = [
391            ("", Some(vec![])),
392            (
393                "  ,  ",
394                Some(vec![ResourceName::empty(), ResourceName::empty()]),
395            ),
396            ("  a  ", Some(vec![ResourceName::new(["a"])])),
397            (
398                "  ,  a  ",
399                Some(vec![ResourceName::empty(), ResourceName::new(["a"])]),
400            ),
401            (
402                "  a  ,  ",
403                Some(vec![ResourceName::new(["a"]), ResourceName::empty()]),
404            ),
405            (
406                "a  ,  b",
407                Some(vec![ResourceName::new(["a"]), ResourceName::new(["b"])]),
408            ),
409            ("`a, b`", Some(vec![ResourceName::new(["a, b"])])),
410            (
411                "a.b, c",
412                Some(vec![
413                    ResourceName::new(["a", "b"]),
414                    ResourceName::new(["c"]),
415                ]),
416            ),
417        ];
418        for (input, expected_output) in cases {
419            let output = ResourceName::parse_column_name_list(input);
420            match (&output, &expected_output) {
421                (Ok(output), Some(expected_output)) => {
422                    assert_eq!(output, expected_output, "from \"{input}\"")
423                }
424                (Err(_), None) => {}
425                _ => panic!("Expected {input} to parse as {expected_output:?}, got {output:?}"),
426            }
427        }
428    }
429}