1use std::cmp::Ordering;
14use std::sync::Arc;
15
16use crate::{
17 Comment, HeldVersion, NativeId, NewComment, Priority, SourceError, Task, TaskSource,
18 TaskUpdate, TaskUpdateOutcome,
19};
20
21#[derive(Debug, Clone, PartialEq)]
25pub struct BatchUpdate {
26 pub id: NativeId,
28 pub update: TaskUpdate,
30 pub if_version: Option<String>,
33 pub when_priority: Vec<Priority>,
36}
37
38#[derive(Debug, Clone, PartialEq)]
41pub struct BatchComment {
42 pub task: NativeId,
44 pub comment: NewComment,
46 pub if_version: Option<String>,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum BatchAnswer<T> {
54 Written(T),
56 VersionMoved {
59 found: Option<String>,
61 },
62 PriorityHeld(Box<Task>),
66 Missing,
68}
69
70pub(crate) async fn update_each<S: TaskSource + ?Sized>(
72 source: &S,
73 updates: &[BatchUpdate],
74) -> Vec<Result<BatchAnswer<TaskUpdateOutcome>, SourceError>> {
75 let mut answers = Vec::with_capacity(updates.len());
76 for each in updates {
77 answers.push(update_one(source, each).await);
78 }
79 answers
80}
81
82async fn update_one<S: TaskSource + ?Sized>(
83 source: &S,
84 each: &BatchUpdate,
85) -> Result<BatchAnswer<TaskUpdateOutcome>, SourceError> {
86 let held = match decide(
87 source,
88 &each.id,
89 each.if_version.as_deref(),
90 &each.when_priority,
91 )
92 .await?
93 {
94 Decided::Answer(answer) => return Ok(answer),
95 Decided::Write(held) => held,
96 };
97 let written = match held {
98 Some(version) if source.holds_versions() => {
99 let hold = Arc::new(HeldVersion::new(each.id.clone(), version));
100 let written = HeldVersion::scope(
101 Arc::clone(&hold),
102 source.update_task(&each.id, &each.update),
103 )
104 .await;
105 if let Some(found) = hold.found() {
106 return Ok(BatchAnswer::VersionMoved { found: Some(found) });
107 }
108 written?
109 }
110 _ => source.update_task(&each.id, &each.update).await?,
111 };
112 Ok(written.map_or(BatchAnswer::Missing, BatchAnswer::Written))
113}
114
115pub(crate) async fn comment_each<S: TaskSource + ?Sized>(
117 source: &S,
118 comments: &[BatchComment],
119) -> Vec<Result<BatchAnswer<Comment>, SourceError>> {
120 let mut answers = Vec::with_capacity(comments.len());
121 for each in comments {
122 answers.push(comment_one(source, each).await);
123 }
124 answers
125}
126
127async fn comment_one<S: TaskSource + ?Sized>(
128 source: &S,
129 each: &BatchComment,
130) -> Result<BatchAnswer<Comment>, SourceError> {
131 let held = match decide(source, &each.task, each.if_version.as_deref(), &[]).await? {
132 Decided::Answer(answer) => return Ok(answer),
133 Decided::Write(held) => held,
134 };
135 let written = match held {
136 Some(version) if source.holds_versions() => {
137 let hold = Arc::new(HeldVersion::new(each.task.clone(), version));
138 let written = HeldVersion::scope(
139 Arc::clone(&hold),
140 source.add_comment(&each.task, &each.comment),
141 )
142 .await;
143 if let Some(found) = hold.found() {
144 return Ok(BatchAnswer::VersionMoved { found: Some(found) });
145 }
146 written?
147 }
148 _ => source.add_comment(&each.task, &each.comment).await?,
149 };
150 Ok(written.map_or(BatchAnswer::Missing, BatchAnswer::Written))
151}
152
153enum Decided<T> {
156 Answer(BatchAnswer<T>),
157 Write(Option<String>),
158}
159
160async fn decide<S: TaskSource + ?Sized, T>(
167 source: &S,
168 id: &NativeId,
169 if_version: Option<&str>,
170 when_priority: &[Priority],
171) -> Result<Decided<T>, SourceError> {
172 if if_version.is_none() && when_priority.is_empty() {
173 return Ok(Decided::Write(None));
174 }
175 if when_priority.is_empty() && source.holds_versions() {
176 return Ok(Decided::Write(if_version.map(str::to_owned)));
177 }
178 let Some(task) = source.get_task(id).await? else {
179 return Ok(Decided::Answer(BatchAnswer::Missing));
180 };
181 if let Some(version) = if_version
182 && !version_at(source, task.version.as_deref(), version)
183 {
184 return Ok(Decided::Answer(BatchAnswer::VersionMoved {
185 found: task.version,
186 }));
187 }
188 if !when_priority.is_empty() && !when_priority.contains(&task.priority) {
189 return Ok(Decided::Answer(BatchAnswer::PriorityHeld(Box::new(task))));
190 }
191 Ok(Decided::Write(task.version))
192}
193
194#[must_use]
196pub fn version_at<S: TaskSource + ?Sized>(source: &S, found: Option<&str>, version: &str) -> bool {
197 found.is_some_and(|found| source.order_versions(found, version) == Some(Ordering::Equal))
198}
199
200#[must_use]
204pub fn decided_on<S: TaskSource + ?Sized, T>(
205 source: &S,
206 task: Option<&Task>,
207 if_version: Option<&str>,
208 when_priority: &[Priority],
209) -> Option<BatchAnswer<T>> {
210 let Some(task) = task else {
211 return Some(BatchAnswer::Missing);
212 };
213 if let Some(version) = if_version
214 && !version_at(source, task.version.as_deref(), version)
215 {
216 return Some(BatchAnswer::VersionMoved {
217 found: task.version.clone(),
218 });
219 }
220 if !when_priority.is_empty() && !when_priority.contains(&task.priority) {
221 return Some(BatchAnswer::PriorityHeld(Box::new(task.clone())));
222 }
223 None
224}