1use crate::error::Result;
2use crate::fields::{same, usage};
3
4#[derive(Clone, Debug, PartialEq)]
5pub enum Op {
6 Set(Vec<String>),
8 Add(String),
10 Remove(String),
12 Clear,
14}
15
16#[derive(Clone, Debug, PartialEq)]
17pub struct Change {
18 pub name: String,
19 pub op: Op,
20}
21
22pub fn parse(specs: &[String]) -> Result<Vec<Change>> {
24 let mut changes: Vec<Change> = Vec::new();
25 for spec in specs {
26 let change = parse_one(spec)?;
27 let conflict = changes
28 .iter()
29 .filter(|other| same(&other.name, &change.name))
30 .any(|other| {
31 !matches!(
32 (&other.op, &change.op),
33 (Op::Set(_), Op::Set(_))
34 | (Op::Add(_) | Op::Remove(_), Op::Add(_) | Op::Remove(_))
35 )
36 });
37 if conflict {
38 return Err(usage(format!(
39 "-f {}: a field is set, cleared, or changed with +=/-=, not two of these at once",
40 change.name
41 )));
42 }
43 if let Op::Set(values) = &change.op
44 && let Some(Change {
45 op: Op::Set(earlier),
46 ..
47 }) = changes
48 .iter_mut()
49 .find(|other| same(&other.name, &change.name))
50 {
51 earlier.extend(values.iter().cloned());
52 continue;
53 }
54 changes.push(change);
55 }
56 Ok(changes)
57}
58
59fn parse_one(spec: &str) -> Result<Change> {
60 let Some(index) = spec.find('=') else {
61 return Err(usage(format!("-f {spec}: expected NAME=VALUE")));
62 };
63 let (head, value) = (&spec[..index], spec[index + 1..].trim());
64 let (name, op) = if let Some(name) = head.strip_suffix('+') {
65 (name, Op::Add(value.to_string()))
66 } else if let Some(name) = head.strip_suffix('-') {
67 (name, Op::Remove(value.to_string()))
68 } else if value.is_empty() {
69 (head, Op::Clear)
70 } else {
71 (head, Op::Set(vec![value.to_string()]))
72 };
73 let name = name.trim();
74 if name.is_empty() {
75 return Err(usage(format!("-f {spec}: the field name is missing")));
76 }
77 if matches!(&op, Op::Add(value) | Op::Remove(value) if value.is_empty()) {
78 return Err(usage(format!("-f {spec}: += and -= need a value")));
79 }
80 Ok(Change {
81 name: name.to_string(),
82 op,
83 })
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn specs(items: &[&str]) -> Vec<String> {
91 items.iter().map(|item| item.to_string()).collect()
92 }
93
94 fn change(name: &str, op: Op) -> Change {
95 Change {
96 name: name.into(),
97 op,
98 }
99 }
100
101 #[test]
102 fn each_form_parses() {
103 let changes = parse(&specs(&[
104 "State=In Progress",
105 "Fix versions+=1.2",
106 "Fix versions-=1.0",
107 "Assignee=",
108 "Note=a=b",
109 ]))
110 .unwrap();
111 assert_eq!(
112 changes,
113 vec![
114 change("State", Op::Set(vec!["In Progress".into()])),
115 change("Fix versions", Op::Add("1.2".into())),
116 change("Fix versions", Op::Remove("1.0".into())),
117 change("Assignee", Op::Clear),
118 change("Note", Op::Set(vec!["a=b".into()])),
119 ]
120 );
121 }
122
123 #[test]
124 fn repeated_sets_of_one_field_form_one_set() {
125 let changes = parse(&specs(&["Fix versions=1.0", "fix VERSIONS=1.1"])).unwrap();
126 assert_eq!(
127 changes,
128 vec![change(
129 "Fix versions",
130 Op::Set(vec!["1.0".into(), "1.1".into()])
131 )]
132 );
133 }
134
135 #[test]
136 fn set_clear_and_add_remove_do_not_mix_for_one_field() {
137 assert!(parse(&specs(&["Fix versions=1.0", "Fix versions+=1.1"])).is_err());
138 assert!(parse(&specs(&["Assignee=", "Assignee=bob"])).is_err());
139 }
140
141 #[test]
142 fn malformed_arguments_are_usage_errors() {
143 for spec in ["Priority", "=Major", "Tags+=", " +=x"] {
144 let error = parse(&specs(&[spec])).unwrap_err();
145 assert_eq!(crate::error::exit_code(&error), 2, "{spec}");
146 }
147 }
148}