omnitrack 0.3.0

Universal issue-tracker provider contracts and clients (Linear, Jira, ...) for Rust, in one crate.
Documentation
use crate::{
    BoxFuture, Comment, CommentId, Comments, ErrorKind, IssueId, IssueResult, Page, PageCursor,
    PageRequest, comment, error,
};
use serde::Deserialize;

use super::client::LinearClient;

const COMMENT_FIELDS: &str = "id body createdAt user { id }";

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CommentNode {
    id: String,
    body: String,
    created_at: Option<String>,
    user: Option<RefNode>,
}

#[derive(Deserialize)]
struct RefNode {
    id: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PageInfo {
    has_next_page: bool,
    end_cursor: Option<String>,
}

fn map_comment(node: CommentNode) -> Comment {
    let mut builder = comment().id(node.id).body(node.body);
    if let Some(user) = node.user {
        builder = builder.author(user.id);
    }
    if let Some(created_at) = node.created_at {
        builder = builder.created_at(created_at);
    }
    builder.build()
}

impl Comments for LinearClient {
    fn list_comments(
        &self,
        issue: IssueId,
        page: Option<PageRequest>,
    ) -> BoxFuture<'_, IssueResult<Page<Comment>>> {
        Box::pin(async move {
            #[derive(Deserialize)]
            struct Data {
                issue: Option<IssueComments>,
            }
            #[derive(Deserialize)]
            struct IssueComments {
                comments: Connection,
            }
            #[derive(Deserialize)]
            struct Connection {
                nodes: Vec<CommentNode>,
                #[serde(rename = "pageInfo")]
                page_info: PageInfo,
            }

            let first = page.as_ref().and_then(PageRequest::limit).unwrap_or(50);
            let after = page
                .as_ref()
                .and_then(PageRequest::after)
                .map(|cursor| cursor.as_str().to_string());

            let query = format!(
                "query($id: String!, $first: Int!, $after: String) {{ issue(id: $id) {{ comments(first: $first, after: $after) {{ nodes {{ {COMMENT_FIELDS} }} pageInfo {{ hasNextPage endCursor }} }} }} }}"
            );
            let data: Data = self
                .execute(
                    &query,
                    serde_json::json!({ "id": issue.as_str(), "first": first, "after": after }),
                )
                .await?;

            let connection = data
                .issue
                .ok_or_else(|| error().of(ErrorKind::NotFound, "linear issue not found"))?
                .comments;

            let items = connection.nodes.into_iter().map(map_comment).collect();
            let next = if connection.page_info.has_next_page {
                connection.page_info.end_cursor.map(PageCursor::make)
            } else {
                None
            };

            Ok(Page::make(items, next))
        })
    }

    fn post_comment(&self, issue: IssueId, body: String) -> BoxFuture<'_, IssueResult<Comment>> {
        Box::pin(async move {
            #[derive(Deserialize)]
            struct Data {
                #[serde(rename = "commentCreate")]
                result: Mutation,
            }
            #[derive(Deserialize)]
            struct Mutation {
                comment: Option<CommentNode>,
            }

            let query = format!(
                "mutation($input: CommentCreateInput!) {{ commentCreate(input: $input) {{ comment {{ {COMMENT_FIELDS} }} }} }}"
            );
            let data: Data = self
                .execute(
                    &query,
                    serde_json::json!({ "input": { "issueId": issue.as_str(), "body": body } }),
                )
                .await?;

            data.result.comment.map(map_comment).ok_or_else(|| {
                error().of(
                    ErrorKind::Provider,
                    "linear commentCreate returned no comment",
                )
            })
        })
    }

    fn delete_comment(&self, id: CommentId) -> BoxFuture<'_, IssueResult<()>> {
        Box::pin(async move {
            #[derive(Deserialize)]
            struct Data {
                #[serde(rename = "commentDelete")]
                result: Mutation,
            }
            #[derive(Deserialize)]
            struct Mutation {
                success: bool,
            }

            let query = "mutation($id: String!) { commentDelete(id: $id) { success } }";
            let data: Data = self
                .execute(query, serde_json::json!({ "id": id.as_str() }))
                .await?;

            if data.result.success {
                Ok(())
            } else {
                Err(error().of(ErrorKind::Provider, "linear commentDelete reported failure"))
            }
        })
    }
}