1use std::str::FromStr;
2
3#[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 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 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}