use onetaskgraph_plugin_api::{
Comment, CommentBody, Cursor, NativeId, NewComment, PageRequest, SourceError, SourceName, Task,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::fetch::{fits, unrepeated};
use super::{ConfiguredSource, Engine, EngineError, Qualified};
use crate::GlobalId;
use crate::plan::{QueryResponse, SourceFailure};
use crate::resolve::ResolvedSource;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CommentList {
pub comments: Vec<Comment>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct DeletedComment {
pub deleted: NativeId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TaskDetail {
#[serde(flatten)]
pub response: QueryResponse<Qualified<Task>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comments: Option<Vec<Comment>>,
}
impl Engine {
pub async fn task_detail(&self, id: &GlobalId) -> Result<TaskDetail, EngineError> {
let mut response = self.task(id).await?;
let source = match self.configured(&id.source) {
Some(ConfiguredSource::Ready(source))
if !response.items.is_empty()
&& source.source().capabilities().comments.is_native() =>
{
source
}
_ => {
return Ok(TaskDetail {
response,
comments: None,
});
}
};
let comments = match walk(source, &id.native).await {
Ok(comments) => comments,
Err(error) => {
response.errors.push(SourceFailure {
source: source.name().clone(),
error,
});
None
}
};
Ok(TaskDetail { response, comments })
}
pub async fn comments(&self, task: &GlobalId) -> Result<CommentList, EngineError> {
let source = self.commented(&task.source)?;
match walk(source, &task.native).await {
Ok(Some(comments)) => Ok(CommentList { comments }),
Ok(None) => Err(no_such_task(task)),
Err(error) => Err(failed(source, error)),
}
}
pub async fn add_comment(
&self,
task: &GlobalId,
comment: &NewComment,
) -> Result<Comment, EngineError> {
let source = self.writable_comments(&task.source)?;
match source.source().add_comment(&task.native, comment).await {
Ok(Some(added)) => Ok(added),
Ok(None) => Err(no_such_task(task)),
Err(error) => Err(failed(source, error)),
}
}
pub async fn edit_comment(
&self,
task: &GlobalId,
comment: &NativeId,
body: &CommentBody,
) -> Result<Comment, EngineError> {
let source = self.writable_comments(&task.source)?;
match source
.source()
.edit_comment(&task.native, comment, body)
.await
{
Ok(Some(edited)) => Ok(edited),
Ok(None) => Err(missing(source, task, comment).await),
Err(error) => Err(failed(source, error)),
}
}
pub async fn delete_comment(
&self,
task: &GlobalId,
comment: &NativeId,
) -> Result<DeletedComment, EngineError> {
let source = self.writable_comments(&task.source)?;
match source.source().delete_comment(&task.native, comment).await {
Ok(Some(deleted)) => Ok(DeletedComment { deleted }),
Ok(None) => Err(missing(source, task, comment).await),
Err(error) => Err(failed(source, error)),
}
}
fn configured(&self, name: &SourceName) -> Option<&ConfiguredSource> {
self.sources.iter().find(|source| source.name() == name)
}
fn commented(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
let name = self.known(name)?;
match self.configured(&name) {
Some(ConfiguredSource::Ready(source)) => {
if source.source().capabilities().comments.is_native() {
Ok(source)
} else {
Err(EngineError::NoComments {
name: name.to_string(),
kind: source.kind().to_owned(),
})
}
}
Some(ConfiguredSource::Unavailable(source)) => Err(EngineError::SourceUnavailable {
name: name.to_string(),
error: source.error().clone(),
}),
None => Err(EngineError::NoSources),
}
}
fn writable_comments(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
let source = self.commented(name)?;
if source.source().writes().is_supported() {
return Ok(source);
}
Err(EngineError::CommentsNotWritable {
name: source.name().to_string(),
kind: source.kind().to_owned(),
})
}
}
async fn walk(
source: &ResolvedSource,
task: &NativeId,
) -> Result<Option<Vec<Comment>>, SourceError> {
let limit = source.source().capabilities().max_page_size.max(1);
let mut comments = Vec::new();
let mut cursor: Option<Cursor> = None;
loop {
let request = PageRequest {
cursor: cursor.clone(),
limit,
};
let Some(page) = source.source().task_comments(task, &request).await? else {
return Ok(None);
};
fits(page.items.len(), limit)?;
unrepeated(
page.next.as_ref(),
cursor.as_ref(),
"walking a task's comments",
)?;
comments.extend(page.items);
match page.next {
Some(next) => cursor = Some(next),
None => return Ok(Some(comments)),
}
}
}
async fn missing(source: &ResolvedSource, task: &GlobalId, comment: &NativeId) -> EngineError {
match source.source().get_task(&task.native).await {
Ok(Some(_)) => EngineError::NoSuchComment {
task: task.to_string(),
comment: comment.to_string(),
},
Ok(None) => no_such_task(task),
Err(error) => failed(source, error),
}
}
fn no_such_task(task: &GlobalId) -> EngineError {
EngineError::NoSuchTask {
id: task.to_string(),
}
}
fn failed(source: &ResolvedSource, error: SourceError) -> EngineError {
EngineError::SourceFailed {
name: source.name().to_string(),
error,
}
}