uv_cli/
comma.rs

1use std::str::FromStr;
2
3/// A comma-separated string of requirements, e.g., `"flask,anyio"`, that takes extras into account
4/// (i.e., treats `"psycopg[binary,pool]"` as a single requirement).
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6pub struct CommaSeparatedRequirements(Vec<String>);
7
8impl IntoIterator for CommaSeparatedRequirements {
9    type Item = String;
10    type IntoIter = std::vec::IntoIter<Self::Item>;
11
12    fn into_iter(self) -> Self::IntoIter {
13        self.0.into_iter()
14    }
15}
16
17impl FromStr for CommaSeparatedRequirements {
18    type Err = String;
19
20    fn from_str(input: &str) -> Result<Self, Self::Err> {
21        // Split on commas _outside_ of brackets.
22        let mut requirements = Vec::new();
23        let mut depth = 0usize;
24        let mut start = 0usize;
25        for (i, c) in input.char_indices() {
26            match c {
27                '[' => {
28                    depth = depth.saturating_add(1);
29                }
30                ']' => {
31                    depth = depth.saturating_sub(1);
32                }
33                ',' if depth == 0 => {
34                    // If the next character is a version identifier, skip the comma, as in:
35                    // `requests>=2.1,<3`.
36                    if let Some(c) = input
37                        .get(i + ','.len_utf8()..)
38                        .and_then(|s| s.chars().find(|c| !c.is_whitespace()))
39                    {
40                        if matches!(c, '!' | '=' | '<' | '>' | '~') {
41                            continue;
42                        }
43                    }
44
45                    let requirement = input[start..i].trim().to_string();
46                    if !requirement.is_empty() {
47                        requirements.push(requirement);
48                    }
49                    start = i + ','.len_utf8();
50                }
51                _ => {}
52            }
53        }
54        let requirement = input[start..].trim().to_string();
55        if !requirement.is_empty() {
56            requirements.push(requirement);
57        }
58        Ok(Self(requirements))
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::CommaSeparatedRequirements;
65    use std::str::FromStr;
66
67    #[test]
68    fn single() {
69        assert_eq!(
70            CommaSeparatedRequirements::from_str("flask").unwrap(),
71            CommaSeparatedRequirements(vec!["flask".to_string()])
72        );
73    }
74
75    #[test]
76    fn double() {
77        assert_eq!(
78            CommaSeparatedRequirements::from_str("flask,anyio").unwrap(),
79            CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()])
80        );
81    }
82
83    #[test]
84    fn empty() {
85        assert_eq!(
86            CommaSeparatedRequirements::from_str("flask,,anyio").unwrap(),
87            CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()])
88        );
89    }
90
91    #[test]
92    fn single_extras() {
93        assert_eq!(
94            CommaSeparatedRequirements::from_str("psycopg[binary,pool]").unwrap(),
95            CommaSeparatedRequirements(vec!["psycopg[binary,pool]".to_string()])
96        );
97    }
98
99    #[test]
100    fn double_extras() {
101        assert_eq!(
102            CommaSeparatedRequirements::from_str("psycopg[binary,pool], flask").unwrap(),
103            CommaSeparatedRequirements(vec![
104                "psycopg[binary,pool]".to_string(),
105                "flask".to_string()
106            ])
107        );
108    }
109
110    #[test]
111    fn single_specifiers() {
112        assert_eq!(
113            CommaSeparatedRequirements::from_str("requests>=2.1,<3").unwrap(),
114            CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string()])
115        );
116    }
117
118    #[test]
119    fn double_specifiers() {
120        assert_eq!(
121            CommaSeparatedRequirements::from_str("requests>=2.1,<3, flask").unwrap(),
122            CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string(), "flask".to_string()])
123        );
124    }
125}