Skip to main content

yt/api/
projection.rs

1use serde::de::DeserializeOwned;
2
3/// A response type together with the `fields` selector that produces exactly what it decodes.
4pub trait Projection: DeserializeOwned {
5    const FIELDS: &'static str;
6}
7
8impl<T: Projection> Projection for Vec<T> {
9    const FIELDS: &'static str = T::FIELDS;
10}
11
12/// Checks a selector's syntax; YouTrack drops unknown names silently, so syntax is all that can be checked here.
13pub fn check_selector(selector: &str) -> std::result::Result<(), String> {
14    let mut depth = 0usize;
15    let mut name = String::new();
16    let mut previous = ' ';
17    for c in selector.chars() {
18        match c {
19            ',' | '(' => {
20                if name.is_empty() && !(c == ',' && previous == ')') {
21                    return Err(format!("empty name before '{c}' in '{selector}'"));
22                }
23                if c == '(' {
24                    depth += 1;
25                }
26                name.clear();
27            }
28            ')' => {
29                if name.is_empty() && previous != ')' {
30                    return Err(format!("empty name before ')' in '{selector}'"));
31                }
32                depth = depth
33                    .checked_sub(1)
34                    .ok_or_else(|| format!("unbalanced ')' in '{selector}'"))?;
35                name.clear();
36            }
37            c if c.is_ascii_alphanumeric() || c == '$' || c == '_' => name.push(c),
38            other => return Err(format!("unexpected '{other}' in '{selector}'")),
39        }
40        previous = c;
41    }
42    if depth != 0 {
43        return Err(format!("unbalanced '(' in '{selector}'"));
44    }
45    if name.is_empty() && previous != ')' {
46        return Err(format!("empty name at the end of '{selector}'"));
47    }
48    Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn valid_selectors() {
57        for selector in [
58            "idReadable",
59            "id,login,name",
60            "project(shortName,name),customFields(name,$type,value(name,login))",
61        ] {
62            assert_eq!(check_selector(selector), Ok(()), "{selector}");
63        }
64    }
65
66    #[test]
67    fn invalid_selectors() {
68        for selector in [
69            "", "id,,name", "project(", "project)", "a(b))", "a b", "(name)", "a,",
70        ] {
71            assert!(check_selector(selector).is_err(), "{selector}");
72        }
73    }
74
75    #[test]
76    fn me_selector_is_valid() {
77        assert_eq!(
78            check_selector(<crate::api::users::Me as Projection>::FIELDS),
79            Ok(())
80        );
81    }
82}