Skip to main content

yt/fields/
change.rs

1use crate::error::Result;
2use crate::fields::{same, usage};
3
4#[derive(Clone, Debug, PartialEq)]
5pub enum Op {
6    /// `NAME=VALUE`, repeated for a multi-value field.
7    Set(Vec<String>),
8    /// `NAME+=VALUE`
9    Add(String),
10    /// `NAME-=VALUE`
11    Remove(String),
12    /// `NAME=`
13    Clear,
14}
15
16#[derive(Clone, Debug, PartialEq)]
17pub struct Change {
18    pub name: String,
19    pub op: Op,
20}
21
22/// Parses `-f` arguments, merging repeated `NAME=VALUE` for one field into one set.
23pub 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}