use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::client::LinearClient;
use crate::error::{Error, Result};
use crate::filter::ProjectFilter;
use crate::ids::{ProjectId, ProjectMilestoneId, ProjectStatusId, TeamId, UserId};
use crate::pagination::{Page, PageInfo};
use crate::types::{
PaginationOrderBy, Priority, ProjectHealth, ProjectStatusType, TeamRef, TimelessDate,
Undefinable, UserRef, ensure_success,
};
macro_rules! project_fragments {
() => {
concat!(
" fragment ProjectFields on Project { id name slugId url description content",
" status { id name type } health priority progress startDate targetDate",
" lead { ...UserRefFields } teams(first: 25) { nodes { ...TeamRefFields } }",
" createdAt updatedAt completedAt canceledAt archivedAt }",
" fragment UserRefFields on User { id name displayName }",
" fragment TeamRefFields on Team { id key name }"
)
};
}
macro_rules! milestone_fragment {
() => {
concat!(
" fragment MilestoneFields on ProjectMilestone",
" { id name description targetDate sortOrder }"
)
};
}
const PROJECT_LIST: &str = concat!(
"query ProjectList($filter: ProjectFilter, $first: Int, $after: String,",
" $includeArchived: Boolean, $orderBy: PaginationOrderBy) {",
" projects(filter: $filter, first: $first, after: $after,",
" includeArchived: $includeArchived, orderBy: $orderBy) {",
" nodes { ...ProjectFields }",
" pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } }",
project_fragments!()
);
const PROJECT_GET: &str = concat!(
"query ProjectGet($id: String!) { project(id: $id) { ...ProjectFields } }",
project_fragments!()
);
const PROJECT_CREATE: &str = concat!(
"mutation ProjectCreate($input: ProjectCreateInput!) {",
" projectCreate(input: $input) { success project { ...ProjectFields } } }",
project_fragments!()
);
const UPDATE_PROJECT: &str = concat!(
"mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {",
" projectUpdate(id: $id, input: $input) { success project { ...ProjectFields } } }",
project_fragments!()
);
const PROJECT_ARCHIVE: &str =
"mutation ProjectArchive($id: String!) { projectArchive(id: $id) { success } }";
const PROJECT_STATUSES: &str =
"query ProjectStatuses { projectStatuses(first: 50) { nodes { id name type } } }";
const PROJECT_MILESTONES: &str = concat!(
"query ProjectMilestones($id: String!, $first: Int, $after: String) {",
" project(id: $id) { projectMilestones(first: $first, after: $after) {",
" nodes { ...MilestoneFields }",
" pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } }",
milestone_fragment!()
);
const PROJECT_MILESTONE_CREATE: &str = concat!(
"mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) {",
" projectMilestoneCreate(input: $input) {",
" success projectMilestone { ...MilestoneFields } } }",
milestone_fragment!()
);
const PROJECT_MILESTONE_UPDATE: &str = concat!(
"mutation ProjectMilestoneUpdate($id: String!, $input: ProjectMilestoneUpdateInput!) {",
" projectMilestoneUpdate(id: $id, input: $input) {",
" success projectMilestone { ...MilestoneFields } } }",
milestone_fragment!()
);
const PROJECT_MILESTONE_DELETE: &str = concat!(
"mutation ProjectMilestoneDelete($id: String!) {",
" projectMilestoneDelete(id: $id) { success } }"
);
pub(crate) const DOCUMENTS: &[(&str, &str)] = &[
("ProjectList", PROJECT_LIST),
("ProjectGet", PROJECT_GET),
("ProjectCreate", PROJECT_CREATE),
("UpdateProject", UPDATE_PROJECT),
("ProjectArchive", PROJECT_ARCHIVE),
("ProjectStatuses", PROJECT_STATUSES),
("ProjectMilestones", PROJECT_MILESTONES),
("ProjectMilestoneCreate", PROJECT_MILESTONE_CREATE),
("ProjectMilestoneUpdate", PROJECT_MILESTONE_UPDATE),
("ProjectMilestoneDelete", PROJECT_MILESTONE_DELETE),
];
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Project {
pub id: ProjectId,
pub name: String,
pub slug_id: String,
pub url: String,
pub description: String,
pub content: Option<String>,
pub status: ProjectStatus,
pub health: Option<ProjectHealth>,
pub priority: Priority,
pub progress: f64,
pub start_date: Option<TimelessDate>,
pub target_date: Option<TimelessDate>,
pub lead: Option<UserRef>,
#[serde(deserialize_with = "crate::types::nodes")]
pub teams: Vec<TeamRef>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: time::OffsetDateTime,
#[serde(with = "time::serde::rfc3339::option", default)]
pub completed_at: Option<time::OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option", default)]
pub canceled_at: Option<time::OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option", default)]
pub archived_at: Option<time::OffsetDateTime>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProjectStatus {
pub id: ProjectStatusId,
pub name: String,
#[serde(rename = "type")]
pub status_type: ProjectStatusType,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProjectMilestone {
pub id: ProjectMilestoneId,
pub name: String,
pub description: Option<String>,
pub target_date: Option<TimelessDate>,
pub sort_order: f64,
}
#[derive(Debug, Clone, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ListProjectsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<ProjectFilter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub first: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(into)]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_archived: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_by: Option<PaginationOrderBy>,
}
#[derive(Debug, Clone, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProjectCreateInput {
pub name: String,
pub team_ids: Vec<TeamId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_id: Option<ProjectStatusId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lead_id: Option<UserId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub member_ids: Option<Vec<UserId>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<Priority>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_date: Option<TimelessDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_date: Option<TimelessDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProjectUpdateInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_id: Option<ProjectStatusId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<Priority>,
#[serde(skip_serializing_if = "Option::is_none")]
pub team_ids: Option<Vec<TeamId>>,
#[serde(skip_serializing_if = "Undefinable::is_undefined", default)]
#[builder(default, into)]
pub content: Undefinable<String>,
#[serde(skip_serializing_if = "Undefinable::is_undefined", default)]
#[builder(default, into)]
pub lead_id: Undefinable<UserId>,
#[serde(skip_serializing_if = "Undefinable::is_undefined", default)]
#[builder(default, into)]
pub start_date: Undefinable<TimelessDate>,
#[serde(skip_serializing_if = "Undefinable::is_undefined", default)]
#[builder(default, into)]
pub target_date: Undefinable<TimelessDate>,
}
#[derive(Debug, Clone, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProjectMilestoneCreateInput {
pub project_id: ProjectId,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_date: Option<TimelessDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_order: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ProjectMilestoneUpdateInput {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_order: Option<f64>,
#[serde(skip_serializing_if = "Undefinable::is_undefined", default)]
#[builder(default, into)]
pub description: Undefinable<String>,
#[serde(skip_serializing_if = "Undefinable::is_undefined", default)]
#[builder(default, into)]
pub target_date: Undefinable<TimelessDate>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Connection<T> {
nodes: Vec<T>,
page_info: PageInfo,
}
impl<T> From<Connection<T>> for Page<T> {
fn from(connection: Connection<T>) -> Self {
Page {
nodes: connection.nodes,
page_info: connection.page_info,
}
}
}
#[derive(Clone, Copy)]
pub struct ProjectsService<'a> {
client: &'a LinearClient,
}
impl LinearClient {
pub fn projects(&self) -> ProjectsService<'_> {
ProjectsService { client: self }
}
}
impl<'a> ProjectsService<'a> {
pub async fn list(&self, request: ListProjectsRequest) -> Result<Page<Project>> {
#[derive(Deserialize)]
struct Data {
projects: Connection<Project>,
}
let data: Data = self
.client
.query("ProjectList", PROJECT_LIST, request)
.await?;
Ok(data.projects.into())
}
pub fn list_stream(
&self,
request: ListProjectsRequest,
) -> impl futures::Stream<Item = Result<Project>> + 'a {
let service = *self;
crate::pagination::paginate(move |after| {
let mut request = request.clone();
if after.is_some() {
request.after = after;
}
async move { service.list(request).await }
})
}
pub async fn get(&self, id: &ProjectId) -> Result<Project> {
#[derive(Deserialize)]
struct Data {
project: Project,
}
let data: Data = self
.client
.query(
"ProjectGet",
PROJECT_GET,
serde_json::json!({ "id": id.as_str() }),
)
.await?;
Ok(data.project)
}
pub async fn create(&self, input: ProjectCreateInput) -> Result<Project> {
#[derive(Deserialize)]
struct Payload {
success: bool,
project: Option<Project>,
}
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectCreate")]
payload: Payload,
}
let data: Data = self
.client
.mutation(
"ProjectCreate",
PROJECT_CREATE,
serde_json::json!({ "input": input }),
)
.await?;
ensure_success("ProjectCreate", data.payload.success)?;
data.payload.project.ok_or(Error::MissingData {
operation: "ProjectCreate",
})
}
pub async fn update(&self, id: &ProjectId, input: ProjectUpdateInput) -> Result<Project> {
#[derive(Deserialize)]
struct Payload {
success: bool,
project: Option<Project>,
}
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectUpdate")]
payload: Payload,
}
let data: Data = self
.client
.mutation(
"UpdateProject",
UPDATE_PROJECT,
serde_json::json!({ "id": id.as_str(), "input": input }),
)
.await?;
ensure_success("UpdateProject", data.payload.success)?;
data.payload.project.ok_or(Error::MissingData {
operation: "UpdateProject",
})
}
pub async fn archive(&self, id: &ProjectId) -> Result<()> {
#[derive(Deserialize)]
struct Payload {
success: bool,
}
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectArchive")]
payload: Payload,
}
let data: Data = self
.client
.mutation(
"ProjectArchive",
PROJECT_ARCHIVE,
serde_json::json!({ "id": id.as_str() }),
)
.await?;
ensure_success("ProjectArchive", data.payload.success)
}
pub async fn statuses(&self) -> Result<Vec<ProjectStatus>> {
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectStatuses", deserialize_with = "crate::types::nodes")]
statuses: Vec<ProjectStatus>,
}
let data: Data = self
.client
.query("ProjectStatuses", PROJECT_STATUSES, serde_json::json!({}))
.await?;
Ok(data.statuses)
}
pub async fn milestones(&self, id: &ProjectId) -> Result<Vec<ProjectMilestone>> {
#[derive(Deserialize)]
struct ProjectNode {
#[serde(rename = "projectMilestones")]
milestones: Connection<ProjectMilestone>,
}
#[derive(Deserialize)]
struct Data {
project: ProjectNode,
}
let service = *self;
let id = id.clone();
crate::pagination::collect_all(
move |after| {
let id = id.clone();
async move {
let data: Data = service
.client
.query(
"ProjectMilestones",
PROJECT_MILESTONES,
serde_json::json!({ "id": id.as_str(), "first": 50, "after": after }),
)
.await?;
Ok(data.project.milestones.into())
}
},
Some(250),
)
.await
}
pub async fn create_milestone(
&self,
input: ProjectMilestoneCreateInput,
) -> Result<ProjectMilestone> {
#[derive(Deserialize)]
struct Payload {
success: bool,
#[serde(rename = "projectMilestone")]
milestone: Option<ProjectMilestone>,
}
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectMilestoneCreate")]
payload: Payload,
}
let data: Data = self
.client
.mutation(
"ProjectMilestoneCreate",
PROJECT_MILESTONE_CREATE,
serde_json::json!({ "input": input }),
)
.await?;
ensure_success("ProjectMilestoneCreate", data.payload.success)?;
data.payload.milestone.ok_or(Error::MissingData {
operation: "ProjectMilestoneCreate",
})
}
pub async fn update_milestone(
&self,
id: &ProjectMilestoneId,
input: ProjectMilestoneUpdateInput,
) -> Result<ProjectMilestone> {
#[derive(Deserialize)]
struct Payload {
success: bool,
#[serde(rename = "projectMilestone")]
milestone: Option<ProjectMilestone>,
}
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectMilestoneUpdate")]
payload: Payload,
}
let data: Data = self
.client
.mutation(
"ProjectMilestoneUpdate",
PROJECT_MILESTONE_UPDATE,
serde_json::json!({ "id": id.as_str(), "input": input }),
)
.await?;
ensure_success("ProjectMilestoneUpdate", data.payload.success)?;
data.payload.milestone.ok_or(Error::MissingData {
operation: "ProjectMilestoneUpdate",
})
}
pub async fn delete_milestone(&self, id: &ProjectMilestoneId) -> Result<()> {
#[derive(Deserialize)]
struct Payload {
success: bool,
}
#[derive(Deserialize)]
struct Data {
#[serde(rename = "projectMilestoneDelete")]
payload: Payload,
}
let data: Data = self
.client
.mutation(
"ProjectMilestoneDelete",
PROJECT_MILESTONE_DELETE,
serde_json::json!({ "id": id.as_str() }),
)
.await?;
ensure_success("ProjectMilestoneDelete", data.payload.success)
}
}