use bon::Builder;
use futures::Stream;
use serde::{Deserialize, Serialize};
use crate::client::LinearClient;
use crate::error::Result;
use crate::filter::{IdComparator, TeamFilter, UserFilter, WorkflowStateFilter};
use crate::ids::{TeamId, UserId, WorkflowStateId};
use crate::pagination::{Page, PageInfo};
use crate::types::{TeamRef, WorkflowStateType};
macro_rules! team_fields_fragment {
() => {
"fragment TeamFields on Team { id key name description color icon private timezone \
cyclesEnabled issueEstimationType defaultIssueEstimate }"
};
}
macro_rules! user_fields_fragment {
() => {
"fragment UserFields on User { id name displayName email active admin guest avatarUrl \
timezone }"
};
}
macro_rules! workflow_state_fields_fragment {
() => {
"fragment WorkflowStateFields on WorkflowState { id name type position color \
description team { ...TeamRefFields } } \
fragment TeamRefFields on Team { id key name }"
};
}
const TEAM_LIST: &str = concat!(
"query TeamList($filter: TeamFilter, $first: Int, $after: String, \
$includeArchived: Boolean) { \
teams(filter: $filter, first: $first, after: $after, includeArchived: $includeArchived) { \
nodes { ...TeamFields } \
pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } ",
team_fields_fragment!()
);
const TEAM_GET: &str = concat!(
"query TeamGet($id: String!) { team(id: $id) { ...TeamFields } } ",
team_fields_fragment!()
);
const WORKFLOW_STATE_LIST: &str = concat!(
"query WorkflowStateList($filter: WorkflowStateFilter, $first: Int, $after: String) { \
workflowStates(filter: $filter, first: $first, after: $after) { \
nodes { ...WorkflowStateFields } \
pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } ",
workflow_state_fields_fragment!()
);
const USER_LIST: &str = concat!(
"query UserList($filter: UserFilter, $first: Int, $after: String, \
$includeDisabled: Boolean) { \
users(filter: $filter, first: $first, after: $after, includeDisabled: $includeDisabled) { \
nodes { ...UserFields } \
pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } ",
user_fields_fragment!()
);
const USER_GET: &str = concat!(
"query UserGet($id: String!) { user(id: $id) { ...UserFields } } ",
user_fields_fragment!()
);
pub(crate) const DOCUMENTS: &[(&str, &str)] = &[
("TeamList", TEAM_LIST),
("TeamGet", TEAM_GET),
("WorkflowStateList", WORKFLOW_STATE_LIST),
("UserList", USER_LIST),
("UserGet", USER_GET),
];
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Team {
pub id: TeamId,
pub key: String,
pub name: String,
pub description: Option<String>,
pub color: Option<String>,
pub icon: Option<String>,
pub private: bool,
pub timezone: String,
pub cycles_enabled: bool,
pub issue_estimation_type: String,
pub default_issue_estimate: f64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct WorkflowState {
pub id: WorkflowStateId,
pub name: String,
#[serde(rename = "type")]
pub state_type: WorkflowStateType,
pub position: f64,
pub color: String,
pub description: Option<String>,
pub team: TeamRef,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct User {
pub id: UserId,
pub name: String,
pub display_name: String,
pub email: String,
pub active: bool,
pub admin: bool,
pub guest: bool,
pub avatar_url: Option<String>,
pub timezone: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ListTeamsRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<TeamFilter>,
#[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>,
}
#[derive(Debug, Clone, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ListWorkflowStatesRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<WorkflowStateFilter>,
#[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, Default, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ListUsersRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<UserFilter>,
#[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_disabled: Option<bool>,
}
#[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(Debug, Clone, Copy)]
pub struct TeamsService<'a> {
client: &'a LinearClient,
}
#[derive(Debug, Clone, Copy)]
pub struct UsersService<'a> {
client: &'a LinearClient,
}
#[derive(Debug, Clone, Copy)]
pub struct WorkflowStatesService<'a> {
client: &'a LinearClient,
}
impl LinearClient {
pub fn teams(&self) -> TeamsService<'_> {
TeamsService { client: self }
}
pub fn users(&self) -> UsersService<'_> {
UsersService { client: self }
}
pub fn workflow_states(&self) -> WorkflowStatesService<'_> {
WorkflowStatesService { client: self }
}
}
impl<'a> TeamsService<'a> {
pub async fn list(&self, req: ListTeamsRequest) -> Result<Page<Team>> {
#[derive(Deserialize)]
struct Data {
teams: Connection<Team>,
}
let data: Data = self.client.query("TeamList", TEAM_LIST, req).await?;
Ok(data.teams.into())
}
pub fn list_stream(self, req: ListTeamsRequest) -> impl Stream<Item = Result<Team>> + 'a {
crate::pagination::paginate(move |after| {
let mut req = req.clone();
if after.is_some() {
req.after = after;
}
async move { self.list(req).await }
})
}
pub async fn get(&self, id: &TeamId) -> Result<Team> {
#[derive(Deserialize)]
struct Data {
team: Team,
}
let data: Data = self
.client
.query(
"TeamGet",
TEAM_GET,
serde_json::json!({ "id": id.as_str() }),
)
.await?;
Ok(data.team)
}
pub async fn states(&self, team: &TeamId) -> Result<Vec<WorkflowState>> {
let filter = WorkflowStateFilter {
team: Some(TeamFilter {
id: Some(IdComparator {
eq: Some(team.to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let service = self.client.workflow_states();
let mut states = crate::pagination::collect_all(
move |after| {
let req = ListWorkflowStatesRequest {
filter: Some(filter.clone()),
first: Some(100),
after,
};
async move { service.list(req).await }
},
Some(100),
)
.await?;
states.sort_by(|a, b| a.position.total_cmp(&b.position));
Ok(states)
}
}
impl WorkflowStatesService<'_> {
pub async fn list(&self, req: ListWorkflowStatesRequest) -> Result<Page<WorkflowState>> {
#[derive(Deserialize)]
struct Data {
#[serde(rename = "workflowStates")]
workflow_states: Connection<WorkflowState>,
}
let data: Data = self
.client
.query("WorkflowStateList", WORKFLOW_STATE_LIST, req)
.await?;
Ok(data.workflow_states.into())
}
}
impl<'a> UsersService<'a> {
pub async fn list(&self, req: ListUsersRequest) -> Result<Page<User>> {
#[derive(Deserialize)]
struct Data {
users: Connection<User>,
}
let data: Data = self.client.query("UserList", USER_LIST, req).await?;
Ok(data.users.into())
}
pub fn list_stream(self, req: ListUsersRequest) -> impl Stream<Item = Result<User>> + 'a {
crate::pagination::paginate(move |after| {
let mut req = req.clone();
if after.is_some() {
req.after = after;
}
async move { self.list(req).await }
})
}
pub async fn get(&self, id: &UserId) -> Result<User> {
#[derive(Deserialize)]
struct Data {
user: User,
}
let data: Data = self
.client
.query(
"UserGet",
USER_GET,
serde_json::json!({ "id": id.as_str() }),
)
.await?;
Ok(data.user)
}
}