From b85f6ee824576dd0deb5f8fead2a874db29e2d44 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 8 Dec 2024 22:15:23 +0000 Subject: [PATCH] feat(trello): add command 'trello stack list' --- src/nextcloud/mod.rs | 2 +- src/nextcloud/tests.rs | 18 ++--- src/tests/mod.rs | 19 ++--- src/tests/responses/trello-stack-list.json | 44 ++++++++++ src/trello/api/lists.rs | 93 ++++++++++++++-------- src/trello/api/members.rs | 6 +- src/trello/api/mod.rs | 2 +- src/trello/client.rs | 9 ++- src/trello/mod.rs | 7 ++ src/trello/model/auth.rs | 13 ++- src/trello/model/board.rs | 3 +- src/trello/model/card.rs | 20 ++--- src/trello/model/mod.rs | 4 +- src/trello/stack.rs | 44 ++++++++++ src/trello/tests.rs | 65 ++++++++++++++- 15 files changed, 260 insertions(+), 89 deletions(-) create mode 100644 src/tests/responses/trello-stack-list.json create mode 100644 src/trello/stack.rs diff --git a/src/nextcloud/mod.rs b/src/nextcloud/mod.rs index 1fd99e9..9084818 100644 --- a/src/nextcloud/mod.rs +++ b/src/nextcloud/mod.rs @@ -10,7 +10,7 @@ use clap::Parser; // mod board; -mod card; +pub(crate) mod card; pub(crate) mod client; pub(crate) mod model; mod stack; diff --git a/src/nextcloud/tests.rs b/src/nextcloud/tests.rs index f54c4e2..70b81c5 100644 --- a/src/nextcloud/tests.rs +++ b/src/nextcloud/tests.rs @@ -1,16 +1,18 @@ // use kxio::net::StatusCode; +use serde_json::json; use crate::{ execute::Execute, nextcloud::{ board::NextcloudBoardCommand, + card::{AddLabel, Create}, model::{ - Card, NextcloudBoardId, NextcloudCardId, NextcloudCardTitle, NextcloudETag, - NextcloudHostname, NextcloudOrder, NextcloudPassword, NextcloudStackId, - NextcloudStackTitle, NextcloudUsername, Stack, + Card, Label, NextcloudBoardId, NextcloudCardId, NextcloudCardTitle, NextcloudETag, + NextcloudHostname, NextcloudLabelId, NextcloudOrder, NextcloudPassword, + NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack, }, - NextcloudCommand, + NextcloudCommand, NextcloudConfig, }, s, tests::given, @@ -19,7 +21,6 @@ use crate::{ mod config { use super::*; - use crate::nextcloud::NextcloudConfig; #[test] fn config_hostname_returns_hostname() { @@ -324,14 +325,7 @@ mod commands { } mod card { - use serde_json::json; - use super::*; - use crate::nextcloud::card::AddLabel; - use crate::nextcloud::{ - card::Create, - model::{Label, NextcloudLabelId}, - }; #[tokio::test] async fn list() { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3c4328b..961b6c0 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,6 +9,7 @@ use crate::{f, init::run, Ctx, NAME}; mod init { use super::*; + use test_log::test; #[test] @@ -110,13 +111,9 @@ mod template { pub(crate) mod given { use super::*; - use crate::config::AppConfig; - use crate::nextcloud::model::{ - NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername, - }; - use crate::nextcloud::NextcloudConfig; - use crate::trello::TrelloConfig; - use crate::{s, FullCtx}; + + use crate::{config::AppConfig, nextcloud::NextcloudConfig, s, trello::TrelloConfig, FullCtx}; + use kxio::{ fs::{FileSystem, TempFileSystem}, net::{MockNet, Net}, @@ -163,10 +160,10 @@ pub(crate) mod given { } pub(crate) fn a_nextcloud_config() -> NextcloudConfig { - let hostname = NextcloudHostname::new("host-name"); - let username = NextcloudUsername::new("username"); - let password = NextcloudPassword::new("password"); - let board_id = NextcloudBoardId::new(2); + let hostname = s!("host-name").into(); + let username = s!("username").into(); + let password = s!("password").into(); + let board_id = 2.into(); NextcloudConfig { hostname, username, diff --git a/src/tests/responses/trello-stack-list.json b/src/tests/responses/trello-stack-list.json new file mode 100644 index 0000000..526d669 --- /dev/null +++ b/src/tests/responses/trello-stack-list.json @@ -0,0 +1,44 @@ +[ + { + "id": "65ad94865aed24f70ecdce4c", + "name": "Backlog", + "closed": false, + "color": null, + "idBoard": "65ad94865aed24f70ecdce4b", + "pos": 65535, + "subscribed": false, + "softLimit": null, + "type": null, + "datasource": { + "filter": false + } + }, + { + "id": "65ad94865aed24f70ecdce4e", + "name": "To Do", + "closed": false, + "color": null, + "idBoard": "65ad94865aed24f70ecdce4b", + "pos": 196607, + "subscribed": false, + "softLimit": null, + "type": null, + "datasource": { + "filter": false + } + }, + { + "id": "65ad94865aed24f70ecdce52", + "name": "Done 🎉", + "closed": false, + "color": null, + "idBoard": "65ad94865aed24f70ecdce4b", + "pos": 393215, + "subscribed": false, + "softLimit": null, + "type": null, + "datasource": { + "filter": false + } + } +] diff --git a/src/trello/api/lists.rs b/src/trello/api/lists.rs index af1aaf8..a56a2ef 100644 --- a/src/trello/api/lists.rs +++ b/src/trello/api/lists.rs @@ -1,47 +1,72 @@ // -use color_eyre::Result; -use kxio::net::Net; +use kxio::{net::Net, print::Printer}; use crate::{ + api_result::APIResult, f, trello::{ - types::{TrelloAuth, TrelloCard, TrelloList}, - url, + model::{auth::TrelloAuth, list::TrelloList, TrelloBoardId}, + url, TrelloConfig, }, }; -/// Get Cards in a List +/// Get Lists in a Board /// -/// https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-cards-get +/// GET /boards/{id}/lists /// -/// GET /lists/{id}/cards -/// -/// List the cards in a list -/// -/// Request -/// -/// Path parameters -/// -/// id TrelloID REQUIRED -/// -/// Responses -/// -/// 200 OK Success -/// -/// application/json -pub(crate) async fn get_lists_cards( - auth: &TrelloAuth, - list: &TrelloList, +/// List all lists in a board +pub(crate) async fn get_board_lists( + cfg: &TrelloConfig, + board_id: &TrelloBoardId, net: &Net, -) -> Result> { - let net_url = url(f!("/lists/{}/cards", **list.id())); - let cards = net - .get(net_url) - .headers(auth.into()) - .send() - .await? - .json() - .await?; - Ok(cards) + prt: &Printer, +) -> APIResult> { + APIResult::new( + net.get(url(f!("/boards/{}/lists", board_id))) + .headers(TrelloAuth::from(cfg).into()) + .header("accept", "application/json") + .header("content-type", "application/json") + .send() + .await, + prt, + ) + .await } + +// /// Get Cards in a List +// /// +// /// https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-cards-get +// /// +// /// GET /lists/{id}/cards +// /// +// /// List the cards in a list +// /// +// /// Request +// /// +// /// Path parameters +// /// +// /// id TrelloID REQUIRED +// /// +// /// Responses +// /// +// /// 200 OK Success +// /// +// /// application/json +// pub(crate) async fn get_lists_cards<'cfg>( +// cfg: &TrelloConfig, +// list: &TrelloList, +// net: &Net, +// prt: &Printer, +// ) -> APIResult> { +// APIResult::new( +// net.get(url(f!("/lists/{}/cards", list.id))) +// .headers(TrelloAuth::from(cfg).into()) +// .header("accept", "application/json") +// .header("content-type", "application/json") +// .send() +// .await, +// prt, +// ) +// .await +// } diff --git a/src/trello/api/members.rs b/src/trello/api/members.rs index 3b84b69..03cdf98 100644 --- a/src/trello/api/members.rs +++ b/src/trello/api/members.rs @@ -43,13 +43,9 @@ pub(crate) async fn get_boards_that_member_belongs_to( net: &Net, prt: &Printer, ) -> APIResult> { - let auth = TrelloAuth { - api_key: &cfg.api_key, - api_secret: &cfg.api_secret, - }; APIResult::new( net.get(url("/members/me/boards?lists=open")) - .headers((&auth).into()) + .headers(TrelloAuth::from(cfg).into()) .header("Accept", "application/json") .send() .await, diff --git a/src/trello/api/mod.rs b/src/trello/api/mod.rs index 5a7fbd9..4726c47 100644 --- a/src/trello/api/mod.rs +++ b/src/trello/api/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod boards; // pub(crate) mod cards; -// pub(crate) mod lists; +pub(crate) mod lists; pub(crate) mod members; #[cfg(test)] diff --git a/src/trello/client.rs b/src/trello/client.rs index f2dd728..7e36a7b 100644 --- a/src/trello/client.rs +++ b/src/trello/client.rs @@ -1,9 +1,12 @@ // use crate::api_result::APIResult; -use crate::trello::model::board::TrelloBoard; +use crate::trello::model::list::TrelloList; use crate::trello::TrelloConfig; +use crate::trello::{api::lists, model::board::TrelloBoard}; use crate::FullCtx; +use super::model::TrelloBoardId; + pub(crate) struct TrelloClient<'ctx> { ctx: &'ctx FullCtx, } @@ -13,6 +16,10 @@ impl<'ctx> TrelloClient<'ctx> { super::api::members::get_boards_that_member_belongs_to(cfg, &self.ctx.net, &self.ctx.prt) .await } + + pub(crate) async fn lists(&self, board_id: &TrelloBoardId) -> APIResult> { + lists::get_board_lists(&self.ctx.cfg.trello, board_id, &self.ctx.net, &self.ctx.prt).await + } } impl TrelloClient<'_> { diff --git a/src/trello/mod.rs b/src/trello/mod.rs index 3defb10..4e8ca9b 100644 --- a/src/trello/mod.rs +++ b/src/trello/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod api; pub(crate) mod boards; pub(crate) mod client; pub(crate) mod model; +pub(crate) mod stack; #[cfg(test)] mod tests; @@ -16,9 +17,11 @@ use crate::{ auth::{TrelloApiKey, TrelloApiSecret}, TrelloBoardName, }, + stack::TrelloStackCommand, }, FullCtx, }; + use clap::Parser; pub(crate) fn url(path: impl Into) -> String { @@ -31,12 +34,16 @@ pub(crate) fn url(path: impl Into) -> String { pub(crate) enum TrelloCommand { #[clap(subcommand)] Board(TrelloBoardCommand), + + #[clap(subcommand)] + Stack(TrelloStackCommand), } impl Execute for TrelloCommand { async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { match self { Self::Board(cmd) => cmd.execute(ctx).await, + Self::Stack(cmd) => cmd.execute(ctx).await, } } } diff --git a/src/trello/model/auth.rs b/src/trello/model/auth.rs index 2cae623..3617062 100644 --- a/src/trello/model/auth.rs +++ b/src/trello/model/auth.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use derive_more::derive::Display; use crate::newtype; +use crate::trello::TrelloConfig; newtype!(TrelloApiKey, String, Display, PartialOrd, Ord, "API Key"); newtype!( @@ -20,8 +21,16 @@ pub(crate) struct TrelloAuth<'cfg> { pub(crate) api_key: &'cfg TrelloApiKey, pub(crate) api_secret: &'cfg TrelloApiSecret, } -impl From<&TrelloAuth<'_>> for HashMap { - fn from(value: &TrelloAuth) -> Self { +impl<'cfg> From<&'cfg TrelloConfig> for TrelloAuth<'cfg> { + fn from(value: &'cfg TrelloConfig) -> Self { + TrelloAuth { + api_key: &value.api_key, + api_secret: &value.api_secret, + } + } +} +impl From> for HashMap { + fn from(value: TrelloAuth) -> Self { HashMap::from([( "Authorization".into(), format!( diff --git a/src/trello/model/board.rs b/src/trello/model/board.rs index 6d70899..d81edfd 100644 --- a/src/trello/model/board.rs +++ b/src/trello/model/board.rs @@ -1,7 +1,8 @@ // -use super::{TrelloBoardId, TrelloBoardName}; use crate::trello::model::list::TrelloList; +use super::{TrelloBoardId, TrelloBoardName}; + #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub(crate) struct TrelloBoard { pub(crate) id: TrelloBoardId, diff --git a/src/trello/model/card.rs b/src/trello/model/card.rs index fb36c42..b8ac1ea 100644 --- a/src/trello/model/card.rs +++ b/src/trello/model/card.rs @@ -1,20 +1,10 @@ // -use crate::trello::{api::cards::TrelloCardUpdate, TrelloCardId, TrelloCardName, TrelloListId}; +use super::{TrelloCardId, TrelloCardName, TrelloListId}; -#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub(crate) struct TrelloCard { - id: TrelloCardId, - name: TrelloCardName, + pub(crate) id: TrelloCardId, + pub(crate) name: TrelloCardName, #[serde(rename = "idList")] - id_list: TrelloListId, -} -impl TrelloCard { - #[cfg(test)] - pub(crate) const fn new(id: TrelloCardId, name: TrelloCardName, id_list: TrelloListId) -> Self { - Self { id, name, id_list } - } - - pub(crate) const fn list_id(&self) -> &TrelloListId { - &self.id_list - } + pub(crate) id_list: TrelloListId, } diff --git a/src/trello/model/mod.rs b/src/trello/model/mod.rs index 8d9df06..e89fa27 100644 --- a/src/trello/model/mod.rs +++ b/src/trello/model/mod.rs @@ -1,6 +1,6 @@ pub(crate) mod auth; pub(crate) mod board; -// mod card; +pub(crate) mod card; pub(crate) mod list; // mod new_card; @@ -10,7 +10,7 @@ use crate::newtype; newtype!(TrelloBoardId, String, Display, "Board ID"); newtype!(TrelloBoardName, String, Display, "Board Name"); -newtype!(TrelloListId, String, "List ID"); +newtype!(TrelloListId, String, Display, "List ID"); newtype!( TrelloListName, String, diff --git a/src/trello/stack.rs b/src/trello/stack.rs new file mode 100644 index 0000000..ad5c480 --- /dev/null +++ b/src/trello/stack.rs @@ -0,0 +1,44 @@ +// +use clap::Parser; +use color_eyre::Result; + +use crate::{execute::Execute, p, FullCtx}; + +#[derive(Parser, Debug)] +pub(crate) enum TrelloStackCommand { + /// List all stacks (lists) in the board + List { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + }, +} + +impl Execute for TrelloStackCommand { + async fn execute(self, ctx: FullCtx) -> Result<()> { + match self { + Self::List { dump } => list(ctx, dump).await, + } + } +} + +async fn list(ctx: FullCtx, dump: bool) -> std::result::Result<(), color_eyre::eyre::Error> { + let client = ctx.trello_client(); + let cfg = &ctx.cfg.trello; + let board = client + .boards(cfg) + .await + .result? + .into_iter() + .find(|b| b.name == cfg.board_name) + .ok_or_else(|| color_eyre::eyre::eyre!("Board not found"))?; + let api_result = client.lists(&board.id).await; + if dump { + p!(ctx.prt, "{}", api_result.text); + } else { + let lists = api_result.result?; + for list in lists { + p!(ctx.prt, "{}", list.name); + } + } + Ok(()) +} diff --git a/src/trello/tests.rs b/src/trello/tests.rs index d235d10..60399c1 100644 --- a/src/trello/tests.rs +++ b/src/trello/tests.rs @@ -1,8 +1,15 @@ // -use crate::trello::{ - api::boards::TrelloBoards, - api::boards::TrelloBoards as _, - model::{board::TrelloBoard, TrelloBoardId, TrelloBoardName}, +use kxio::net::StatusCode; + +use crate::{ + tests::given, + trello::{ + api::boards::TrelloBoards as _, + model::{ + board::TrelloBoard, list::TrelloList, TrelloBoardId, TrelloBoardName, TrelloListId, + TrelloListName, + }, + }, }; mod commands { @@ -40,4 +47,54 @@ mod commands { assert_eq!(result, Some(&board)); } } + + mod stack { + + use super::*; + + #[tokio::test] + async fn list() { + //given + let mock_net = kxio::net::mock(); + + mock_net + .on() + .get("https://api.trello.com/1/boards/123/lists") + .header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"") + .header("accept", "application/json") + .header("content-type", "application/json") + .respond(StatusCode::OK) + .body(include_str!("../tests/responses/trello-stack-list.json")) + .expect("mock request"); + + // let fs = given::a_filesystem(); + let ctx = given::a_full_context(mock_net); + + //when + let result = ctx + .trello_client() + .lists(&TrelloBoardId::new("123")) + .await + .result + .expect("get stacks"); + + assert_eq!( + result, + vec![ + TrelloList { + id: TrelloListId::new("65ad94865aed24f70ecdce4c"), + name: TrelloListName::new("Backlog") + }, + TrelloList { + id: TrelloListId::new("65ad94865aed24f70ecdce4e"), + name: TrelloListName::new("To Do") + }, + TrelloList { + id: TrelloListId::new("65ad94865aed24f70ecdce52"), + name: TrelloListName::new("Done 🎉") + } + ] + ); + } + } }