Skip to main content

yt/fields/
apply.rs

1use serde_json::{Value, json};
2
3use crate::api::{Client, commands, issues};
4use crate::error::Result;
5use crate::fields::plan::Operation;
6
7/// Runs `operations` in order and stops at the first failure; the error then says what was already applied.
8pub async fn execute(client: &Client, operations: &[Operation]) -> Result<()> {
9    let mut applied: Vec<String> = Vec::new();
10    for operation in operations {
11        let result = match operation {
12            Operation::Command { issues, command } => {
13                commands::apply(client, issues, command).await
14            }
15            Operation::Update { issue, body, .. } => issues::update(client, issue, body).await,
16        };
17        if let Err(error) = result {
18            return Err(if applied.is_empty() {
19                error
20            } else {
21                error.wrap_err(format!(
22                    "stopped after a failure; already applied: {}",
23                    applied.join("; ")
24                ))
25            });
26        }
27        applied.push(operation.describe());
28    }
29    Ok(())
30}
31
32/// An operation and the problems YouTrack sees in it.
33#[derive(Clone, Debug, PartialEq)]
34pub struct Planned {
35    pub operation: Operation,
36    pub problems: Vec<String>,
37}
38
39impl Planned {
40    pub fn json(&self) -> Value {
41        match &self.operation {
42            Operation::Command { issues, command } => json!({
43                "kind": "command",
44                "issues": issues,
45                "command": command,
46                "problems": self.problems,
47            }),
48            Operation::Update { issue, body, .. } => json!({
49                "kind": "update",
50                "issues": [issue],
51                "body": body,
52                "problems": self.problems,
53            }),
54        }
55    }
56}
57
58/// Checks commands with `commands/assist` without applying anything; REST updates have nothing to check.
59pub async fn check(client: &Client, operations: &[Operation]) -> Result<Vec<Planned>> {
60    let mut planned = Vec::new();
61    for operation in operations {
62        let problems = match operation {
63            Operation::Command { issues, command } => commands::assist(client, issues, command)
64                .await?
65                .into_iter()
66                .filter(|parsed| parsed.error)
67                .map(|parsed| parsed.description)
68                .collect(),
69            Operation::Update { .. } => Vec::new(),
70        };
71        planned.push(Planned {
72            operation: operation.clone(),
73            problems,
74        });
75    }
76    Ok(planned)
77}
78
79#[cfg(test)]
80mod tests {
81    use serde_json::json;
82    use wiremock::matchers::{method, path};
83    use wiremock::{Mock, MockServer, ResponseTemplate};
84
85    use super::*;
86    use crate::api::test_client;
87
88    fn operations() -> Vec<Operation> {
89        vec![
90            Operation::Update {
91                issue: "WEB-1".into(),
92                body: json!({"summary": "x"}),
93                summary: "set the summary".into(),
94            },
95            Operation::Command {
96                issues: vec!["WEB-1".into()],
97                command: "Priority Major".into(),
98            },
99        ]
100    }
101
102    #[tokio::test]
103    async fn operations_run_in_order() {
104        let server = MockServer::start().await;
105        Mock::given(method("POST"))
106            .and(path("/api/issues/WEB-1"))
107            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
108            .expect(1)
109            .mount(&server)
110            .await;
111        Mock::given(method("POST"))
112            .and(path("/api/commands"))
113            .respond_with(ResponseTemplate::new(200))
114            .expect(1)
115            .mount(&server)
116            .await;
117        execute(&test_client(&server.uri()), &operations())
118            .await
119            .unwrap();
120    }
121
122    #[tokio::test]
123    async fn a_failure_says_what_was_already_applied() {
124        let server = MockServer::start().await;
125        Mock::given(method("POST"))
126            .and(path("/api/issues/WEB-1"))
127            .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
128            .mount(&server)
129            .await;
130        Mock::given(method("POST"))
131            .and(path("/api/commands"))
132            .respond_with(
133                ResponseTemplate::new(400)
134                    .set_body_json(json!({"error": "bad", "error_description": "Unknown command"})),
135            )
136            .mount(&server)
137            .await;
138        let error = execute(&test_client(&server.uri()), &operations())
139            .await
140            .unwrap_err();
141        let text = format!("{error}");
142        assert!(
143            text.contains("stopped after a failure; already applied: WEB-1: set the summary"),
144            "{text}"
145        );
146        assert_eq!(crate::error::exit_code(&error), 1);
147    }
148
149    #[tokio::test]
150    async fn a_dry_run_asks_youtrack_about_commands_and_changes_nothing() {
151        let server = MockServer::start().await;
152        Mock::given(method("POST"))
153            .and(path("/api/commands/assist"))
154            .respond_with(
155                ResponseTemplate::new(200).set_body_json(json!({"commands": [
156                    {"description": "Unknown value <b>Majr</b>", "error": true}
157                ]})),
158            )
159            .expect(1)
160            .mount(&server)
161            .await;
162        let client = test_client(&server.uri());
163        let planned = check(&client, &operations()).await.unwrap();
164        assert_eq!(planned[0].problems, Vec::<String>::new());
165        assert_eq!(planned[1].problems, ["Unknown value Majr"]);
166        assert!(!client.has_written());
167        assert_eq!(
168            planned[1].json(),
169            json!({"kind": "command", "issues": ["WEB-1"], "command": "Priority Major", "problems": ["Unknown value Majr"]})
170        );
171    }
172}