1use serde_json::{Value, json};
2
3use crate::api::{Client, commands, issues};
4use crate::error::Result;
5use crate::fields::plan::Operation;
6
7pub 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#[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
58pub 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}