use bon::Builder;
use futures::Stream;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::client::LinearClient;
use crate::error::{Error, Result};
use crate::ids::{CommentId, IssueRef};
use crate::pagination::{Page, PageInfo};
use crate::types::{UserRef, ensure_success};
macro_rules! with_comment_fragments {
($op:literal) => {
concat!(
$op,
" fragment CommentFields on Comment {",
" id body url user { ...UserRefFields }",
" createdAt editedAt resolvedAt parent { id } }",
" fragment UserRefFields on User { id name displayName }",
)
};
}
const COMMENTS_FOR_ISSUE: &str = with_comment_fragments!(
"query CommentsForIssue($id: String!, $first: Int, $after: String) { \
issue(id: $id) { comments(first: $first, after: $after) { \
nodes { ...CommentFields } \
pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } }"
);
const COMMENT_CREATE: &str = with_comment_fragments!(
"mutation CommentCreate($input: CommentCreateInput!) { \
commentCreate(input: $input) { success comment { ...CommentFields } } }"
);
const COMMENT_UPDATE: &str = with_comment_fragments!(
"mutation CommentUpdate($id: String!, $input: CommentUpdateInput!) { \
commentUpdate(id: $id, input: $input) { success comment { ...CommentFields } } }"
);
const COMMENT_DELETE: &str =
"mutation CommentDelete($id: String!) { commentDelete(id: $id) { success } }";
pub(crate) const DOCUMENTS: &[(&str, &str)] = &[
("CommentsForIssue", COMMENTS_FOR_ISSUE),
("CommentCreate", COMMENT_CREATE),
("CommentUpdate", COMMENT_UPDATE),
("CommentDelete", COMMENT_DELETE),
];
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Comment {
pub id: CommentId,
pub body: String,
pub url: String,
#[serde(default)]
pub user: Option<UserRef>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(default, with = "time::serde::rfc3339::option")]
pub edited_at: Option<OffsetDateTime>,
#[serde(default, with = "time::serde::rfc3339::option")]
pub resolved_at: Option<OffsetDateTime>,
#[serde(default)]
pub parent: Option<CommentParent>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CommentParent {
pub id: CommentId,
}
#[derive(Debug, Clone, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CommentListRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub first: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(into)]
pub after: Option<String>,
}
#[derive(Debug, Clone, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CommentCreateInput {
#[builder(into)]
pub issue_id: String,
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<CommentId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub create_as_user: Option<String>,
}
#[derive(Serialize)]
struct CommentUpdateInput {
body: String,
}
#[derive(Clone, Copy)]
pub struct CommentsService<'a> {
client: &'a LinearClient,
}
impl LinearClient {
pub fn comments(&self) -> CommentsService<'_> {
CommentsService { client: self }
}
}
impl<'a> CommentsService<'a> {
pub async fn list_for_issue(
&self,
issue: impl Into<IssueRef>,
req: CommentListRequest,
) -> Result<Page<Comment>> {
#[derive(Serialize)]
struct Vars<'v> {
id: &'v str,
#[serde(skip_serializing_if = "Option::is_none")]
first: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
after: Option<&'v str>,
}
#[derive(Deserialize)]
struct Data {
issue: IssueNode,
}
#[derive(Deserialize)]
struct IssueNode {
comments: Connection,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Connection {
nodes: Vec<Comment>,
page_info: PageInfo,
}
let issue = issue.into();
let data: Data = self
.client
.query(
"CommentsForIssue",
COMMENTS_FOR_ISSUE,
Vars {
id: issue.api_string(),
first: req.first,
after: req.after.as_deref(),
},
)
.await?;
Ok(Page {
nodes: data.issue.comments.nodes,
page_info: data.issue.comments.page_info,
})
}
pub fn list_for_issue_stream(
&self,
issue: impl Into<IssueRef>,
req: CommentListRequest,
) -> impl Stream<Item = Result<Comment>> + 'a {
let service = *self;
let issue = issue.into();
crate::pagination::paginate(move |cursor| {
let mut req = req.clone();
if cursor.is_some() {
req.after = cursor;
}
let issue = issue.clone();
async move { service.list_for_issue(issue, req).await }
})
}
pub async fn create(&self, input: CommentCreateInput) -> Result<Comment> {
#[derive(Serialize)]
struct Vars {
input: CommentCreateInput,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Data {
comment_create: CommentPayload,
}
let data: Data = self
.client
.mutation("CommentCreate", COMMENT_CREATE, Vars { input })
.await?;
data.comment_create.into_comment("CommentCreate")
}
pub async fn create_on(
&self,
issue: impl Into<IssueRef>,
body: impl Into<String>,
) -> Result<Comment> {
let issue = issue.into();
self.create(
CommentCreateInput::builder()
.issue_id(issue.api_string())
.body(body.into())
.build(),
)
.await
}
pub async fn update(&self, id: &CommentId, body: impl Into<String>) -> Result<Comment> {
#[derive(Serialize)]
struct Vars<'v> {
id: &'v str,
input: CommentUpdateInput,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Data {
comment_update: CommentPayload,
}
let data: Data = self
.client
.mutation(
"CommentUpdate",
COMMENT_UPDATE,
Vars {
id: id.as_str(),
input: CommentUpdateInput { body: body.into() },
},
)
.await?;
data.comment_update.into_comment("CommentUpdate")
}
pub async fn delete(&self, id: &CommentId) -> Result<()> {
#[derive(Serialize)]
struct Vars<'v> {
id: &'v str,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Data {
comment_delete: DeletePayload,
}
#[derive(Deserialize)]
struct DeletePayload {
success: bool,
}
let data: Data = self
.client
.mutation("CommentDelete", COMMENT_DELETE, Vars { id: id.as_str() })
.await?;
ensure_success("CommentDelete", data.comment_delete.success)
}
}
#[derive(Deserialize)]
struct CommentPayload {
success: bool,
#[serde(default)]
comment: Option<Comment>,
}
impl CommentPayload {
fn into_comment(self, operation: &'static str) -> Result<Comment> {
ensure_success(operation, self.success)?;
self.comment.ok_or(Error::MissingData { operation })
}
}