use std::cmp::Ordering;
use std::sync::Arc;
use crate::{
Comment, HeldVersion, NativeId, NewComment, Priority, SourceError, Task, TaskSource,
TaskUpdate, TaskUpdateOutcome,
};
#[derive(Debug, Clone, PartialEq)]
pub struct BatchUpdate {
pub id: NativeId,
pub update: TaskUpdate,
pub if_version: Option<String>,
pub when_priority: Vec<Priority>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BatchComment {
pub task: NativeId,
pub comment: NewComment,
pub if_version: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BatchAnswer<T> {
Written(T),
VersionMoved {
found: Option<String>,
},
PriorityHeld(Box<Task>),
Missing,
}
pub(crate) async fn update_each<S: TaskSource + ?Sized>(
source: &S,
updates: &[BatchUpdate],
) -> Vec<Result<BatchAnswer<TaskUpdateOutcome>, SourceError>> {
let mut answers = Vec::with_capacity(updates.len());
for each in updates {
answers.push(update_one(source, each).await);
}
answers
}
async fn update_one<S: TaskSource + ?Sized>(
source: &S,
each: &BatchUpdate,
) -> Result<BatchAnswer<TaskUpdateOutcome>, SourceError> {
let held = match decide(
source,
&each.id,
each.if_version.as_deref(),
&each.when_priority,
)
.await?
{
Decided::Answer(answer) => return Ok(answer),
Decided::Write(held) => held,
};
let written = match held {
Some(version) if source.holds_versions() => {
let hold = Arc::new(HeldVersion::new(each.id.clone(), version));
let written = HeldVersion::scope(
Arc::clone(&hold),
source.update_task(&each.id, &each.update),
)
.await;
if let Some(found) = hold.found() {
return Ok(BatchAnswer::VersionMoved { found: Some(found) });
}
written?
}
_ => source.update_task(&each.id, &each.update).await?,
};
Ok(written.map_or(BatchAnswer::Missing, BatchAnswer::Written))
}
pub(crate) async fn comment_each<S: TaskSource + ?Sized>(
source: &S,
comments: &[BatchComment],
) -> Vec<Result<BatchAnswer<Comment>, SourceError>> {
let mut answers = Vec::with_capacity(comments.len());
for each in comments {
answers.push(comment_one(source, each).await);
}
answers
}
async fn comment_one<S: TaskSource + ?Sized>(
source: &S,
each: &BatchComment,
) -> Result<BatchAnswer<Comment>, SourceError> {
let held = match decide(source, &each.task, each.if_version.as_deref(), &[]).await? {
Decided::Answer(answer) => return Ok(answer),
Decided::Write(held) => held,
};
let written = match held {
Some(version) if source.holds_versions() => {
let hold = Arc::new(HeldVersion::new(each.task.clone(), version));
let written = HeldVersion::scope(
Arc::clone(&hold),
source.add_comment(&each.task, &each.comment),
)
.await;
if let Some(found) = hold.found() {
return Ok(BatchAnswer::VersionMoved { found: Some(found) });
}
written?
}
_ => source.add_comment(&each.task, &each.comment).await?,
};
Ok(written.map_or(BatchAnswer::Missing, BatchAnswer::Written))
}
enum Decided<T> {
Answer(BatchAnswer<T>),
Write(Option<String>),
}
async fn decide<S: TaskSource + ?Sized, T>(
source: &S,
id: &NativeId,
if_version: Option<&str>,
when_priority: &[Priority],
) -> Result<Decided<T>, SourceError> {
if if_version.is_none() && when_priority.is_empty() {
return Ok(Decided::Write(None));
}
if when_priority.is_empty() && source.holds_versions() {
return Ok(Decided::Write(if_version.map(str::to_owned)));
}
let Some(task) = source.get_task(id).await? else {
return Ok(Decided::Answer(BatchAnswer::Missing));
};
if let Some(version) = if_version
&& !version_at(source, task.version.as_deref(), version)
{
return Ok(Decided::Answer(BatchAnswer::VersionMoved {
found: task.version,
}));
}
if !when_priority.is_empty() && !when_priority.contains(&task.priority) {
return Ok(Decided::Answer(BatchAnswer::PriorityHeld(Box::new(task))));
}
Ok(Decided::Write(task.version))
}
#[must_use]
pub fn version_at<S: TaskSource + ?Sized>(source: &S, found: Option<&str>, version: &str) -> bool {
found.is_some_and(|found| source.order_versions(found, version) == Some(Ordering::Equal))
}
#[must_use]
pub fn decided_on<S: TaskSource + ?Sized, T>(
source: &S,
task: Option<&Task>,
if_version: Option<&str>,
when_priority: &[Priority],
) -> Option<BatchAnswer<T>> {
let Some(task) = task else {
return Some(BatchAnswer::Missing);
};
if let Some(version) = if_version
&& !version_at(source, task.version.as_deref(), version)
{
return Some(BatchAnswer::VersionMoved {
found: task.version.clone(),
});
}
if !when_priority.is_empty() && !when_priority.contains(&task.priority) {
return Some(BatchAnswer::PriorityHeld(Box::new(task.clone())));
}
None
}