feat(trello): add command 'trello stack list'

This commit is contained in:
Paul Campbell 2024-12-08 22:15:23 +00:00
parent 4e5e35c1da
commit 95b043213e
15 changed files with 256 additions and 87 deletions

View file

@ -21,7 +21,7 @@ use crate::{
}; };
mod board; mod board;
mod card; pub(crate) mod card;
pub(crate) mod client; pub(crate) mod client;
pub(crate) mod model; pub(crate) mod model;
mod stack; mod stack;

View file

@ -1,16 +1,18 @@
// //
use kxio::net::StatusCode; use kxio::net::StatusCode;
use serde_json::json;
use crate::{ use crate::{
execute::Execute, execute::Execute,
nextcloud::{ nextcloud::{
board::NextcloudBoardCommand, board::NextcloudBoardCommand,
card::{AddLabel, Create},
model::{ model::{
Card, NextcloudBoardId, NextcloudCardId, NextcloudCardTitle, NextcloudETag, Card, Label, NextcloudBoardId, NextcloudCardId, NextcloudCardTitle, NextcloudETag,
NextcloudHostname, NextcloudOrder, NextcloudPassword, NextcloudStackId, NextcloudHostname, NextcloudLabelId, NextcloudOrder, NextcloudPassword,
NextcloudStackTitle, NextcloudUsername, Stack, NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack,
}, },
NextcloudCommand, NextcloudCommand, NextcloudConfig,
}, },
s, s,
tests::given, tests::given,
@ -19,7 +21,6 @@ use crate::{
mod config { mod config {
use super::*; use super::*;
use crate::nextcloud::NextcloudConfig;
#[test] #[test]
fn config_hostname_returns_hostname() { fn config_hostname_returns_hostname() {
@ -316,14 +317,7 @@ mod commands {
} }
mod card { mod card {
use serde_json::json;
use super::*; use super::*;
use crate::nextcloud::card::AddLabel;
use crate::nextcloud::{
card::Create,
model::{Label, NextcloudLabelId},
};
#[tokio::test] #[tokio::test]
async fn list() { async fn list() {

View file

@ -61,6 +61,7 @@ mod config {
mod init { mod init {
use super::*; use super::*;
use test_log::test; use test_log::test;
#[test] #[test]
@ -117,13 +118,9 @@ mod template {
pub(crate) mod given { pub(crate) mod given {
use super::*; use super::*;
use crate::config::AppConfig;
use crate::nextcloud::model::{ use crate::{config::AppConfig, nextcloud::NextcloudConfig, s, trello::TrelloConfig, FullCtx};
NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername,
};
use crate::nextcloud::NextcloudConfig;
use crate::trello::TrelloConfig;
use crate::{s, FullCtx};
use kxio::{ use kxio::{
fs::{FileSystem, TempFileSystem}, fs::{FileSystem, TempFileSystem},
net::{MockNet, Net}, net::{MockNet, Net},
@ -170,10 +167,10 @@ pub(crate) mod given {
} }
pub(crate) fn a_nextcloud_config() -> NextcloudConfig { pub(crate) fn a_nextcloud_config() -> NextcloudConfig {
let hostname = NextcloudHostname::new("host-name"); let hostname = s!("host-name").into();
let username = NextcloudUsername::new("username"); let username = s!("username").into();
let password = NextcloudPassword::new("password"); let password = s!("password").into();
let board_id = NextcloudBoardId::new(2); let board_id = 2.into();
NextcloudConfig { NextcloudConfig {
hostname, hostname,
username, username,

View file

@ -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
}
}
]

View file

@ -1,47 +1,72 @@
// //
use color_eyre::Result; use kxio::{net::Net, print::Printer};
use kxio::net::Net;
use crate::{ use crate::{
api_result::APIResult,
f, f,
trello::{ trello::{
types::{TrelloAuth, TrelloCard, TrelloList}, model::{auth::TrelloAuth, list::TrelloList, TrelloBoardId},
url, 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 all lists in a board
/// pub(crate) async fn get_board_lists(
/// List the cards in a list cfg: &TrelloConfig,
/// board_id: &TrelloBoardId,
/// Request
///
/// Path parameters
///
/// id TrelloID REQUIRED
///
/// Responses
///
/// 200 OK Success
///
/// application/json
pub(crate) async fn get_lists_cards(
auth: &TrelloAuth,
list: &TrelloList,
net: &Net, net: &Net,
) -> Result<Vec<TrelloCard>> { prt: &Printer,
let net_url = url(f!("/lists/{}/cards", **list.id())); ) -> APIResult<Vec<TrelloList>> {
let cards = net APIResult::new(
.get(net_url) net.get(url(f!("/boards/{}/lists", board_id)))
.headers(auth.into()) .headers(TrelloAuth::from(cfg).into())
.header("accept", "application/json")
.header("content-type", "application/json")
.send() .send()
.await? .await,
.json() prt,
.await?; )
Ok(cards) .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<Vec<TrelloCard>> {
// 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
// }

View file

@ -43,13 +43,9 @@ pub(crate) async fn get_boards_that_member_belongs_to(
net: &Net, net: &Net,
prt: &Printer, prt: &Printer,
) -> APIResult<Vec<TrelloBoard>> { ) -> APIResult<Vec<TrelloBoard>> {
let auth = TrelloAuth {
api_key: &cfg.api_key,
api_secret: &cfg.api_secret,
};
APIResult::new( APIResult::new(
net.get(url("/members/me/boards?lists=open")) net.get(url("/members/me/boards?lists=open"))
.headers((&auth).into()) .headers(TrelloAuth::from(cfg).into())
.header("Accept", "application/json") .header("Accept", "application/json")
.send() .send()
.await, .await,

View file

@ -2,7 +2,7 @@
pub(crate) mod boards; pub(crate) mod boards;
// pub(crate) mod cards; // pub(crate) mod cards;
// pub(crate) mod lists; pub(crate) mod lists;
pub(crate) mod members; pub(crate) mod members;
#[cfg(test)] #[cfg(test)]

View file

@ -1,9 +1,12 @@
// //
use crate::api_result::APIResult; use crate::api_result::APIResult;
use crate::trello::model::board::TrelloBoard; use crate::trello::model::list::TrelloList;
use crate::trello::TrelloConfig; use crate::trello::TrelloConfig;
use crate::trello::{api::lists, model::board::TrelloBoard};
use crate::FullCtx; use crate::FullCtx;
use super::model::TrelloBoardId;
pub(crate) struct TrelloClient<'ctx> { pub(crate) struct TrelloClient<'ctx> {
ctx: &'ctx FullCtx, 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) super::api::members::get_boards_that_member_belongs_to(cfg, &self.ctx.net, &self.ctx.prt)
.await .await
} }
pub(crate) async fn lists(&self, board_id: &TrelloBoardId) -> APIResult<Vec<TrelloList>> {
lists::get_board_lists(&self.ctx.cfg.trello, board_id, &self.ctx.net, &self.ctx.prt).await
}
} }
impl TrelloClient<'_> { impl TrelloClient<'_> {

View file

@ -3,6 +3,7 @@ pub(crate) mod api;
pub(crate) mod boards; pub(crate) mod boards;
pub(crate) mod client; pub(crate) mod client;
pub(crate) mod model; pub(crate) mod model;
pub(crate) mod stack;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -16,9 +17,11 @@ use crate::{
auth::{TrelloApiKey, TrelloApiSecret}, auth::{TrelloApiKey, TrelloApiSecret},
TrelloBoardName, TrelloBoardName,
}, },
stack::TrelloStackCommand,
}, },
FullCtx, FullCtx,
}; };
use clap::Parser; use clap::Parser;
pub(crate) fn url(path: impl Into<String>) -> String { pub(crate) fn url(path: impl Into<String>) -> String {
@ -31,12 +34,16 @@ pub(crate) fn url(path: impl Into<String>) -> String {
pub(crate) enum TrelloCommand { pub(crate) enum TrelloCommand {
#[clap(subcommand)] #[clap(subcommand)]
Board(TrelloBoardCommand), Board(TrelloBoardCommand),
#[clap(subcommand)]
Stack(TrelloStackCommand),
} }
impl Execute for TrelloCommand { impl Execute for TrelloCommand {
async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> {
match self { match self {
Self::Board(cmd) => cmd.execute(ctx).await, Self::Board(cmd) => cmd.execute(ctx).await,
Self::Stack(cmd) => cmd.execute(ctx).await,
} }
} }
} }

View file

@ -4,6 +4,7 @@ use std::collections::HashMap;
use derive_more::derive::Display; use derive_more::derive::Display;
use crate::newtype; use crate::newtype;
use crate::trello::TrelloConfig;
newtype!(TrelloApiKey, String, Display, PartialOrd, Ord, "API Key"); newtype!(TrelloApiKey, String, Display, PartialOrd, Ord, "API Key");
newtype!( newtype!(
@ -20,8 +21,16 @@ pub(crate) struct TrelloAuth<'cfg> {
pub(crate) api_key: &'cfg TrelloApiKey, pub(crate) api_key: &'cfg TrelloApiKey,
pub(crate) api_secret: &'cfg TrelloApiSecret, pub(crate) api_secret: &'cfg TrelloApiSecret,
} }
impl From<&TrelloAuth<'_>> for HashMap<String, String> { impl<'cfg> From<&'cfg TrelloConfig> for TrelloAuth<'cfg> {
fn from(value: &TrelloAuth) -> Self { fn from(value: &'cfg TrelloConfig) -> Self {
TrelloAuth {
api_key: &value.api_key,
api_secret: &value.api_secret,
}
}
}
impl From<TrelloAuth<'_>> for HashMap<String, String> {
fn from(value: TrelloAuth) -> Self {
HashMap::from([( HashMap::from([(
"Authorization".into(), "Authorization".into(),
format!( format!(

View file

@ -1,7 +1,8 @@
// //
use super::{TrelloBoardId, TrelloBoardName};
use crate::trello::model::list::TrelloList; use crate::trello::model::list::TrelloList;
use super::{TrelloBoardId, TrelloBoardName};
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub(crate) struct TrelloBoard { pub(crate) struct TrelloBoard {
pub(crate) id: TrelloBoardId, pub(crate) id: TrelloBoardId,

View file

@ -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 { pub(crate) struct TrelloCard {
id: TrelloCardId, pub(crate) id: TrelloCardId,
name: TrelloCardName, pub(crate) name: TrelloCardName,
#[serde(rename = "idList")] #[serde(rename = "idList")]
id_list: TrelloListId, pub(crate) 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
}
} }

View file

@ -1,6 +1,6 @@
pub(crate) mod auth; pub(crate) mod auth;
pub(crate) mod board; pub(crate) mod board;
// mod card; pub(crate) mod card;
pub(crate) mod list; pub(crate) mod list;
// mod new_card; // mod new_card;
@ -10,7 +10,7 @@ use crate::newtype;
newtype!(TrelloBoardId, String, Display, "Board ID"); newtype!(TrelloBoardId, String, Display, "Board ID");
newtype!(TrelloBoardName, String, Display, "Board Name"); newtype!(TrelloBoardName, String, Display, "Board Name");
newtype!(TrelloListId, String, "List ID"); newtype!(TrelloListId, String, Display, "List ID");
newtype!( newtype!(
TrelloListName, TrelloListName,
String, String,

44
src/trello/stack.rs Normal file
View file

@ -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(())
}

View file

@ -1,11 +1,16 @@
// //
use kxio::net::StatusCode;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
s, s,
tests::given,
trello::{ trello::{
api::boards::TrelloBoards as _, api::boards::TrelloBoards as _,
model::{auth::TrelloAuth, board::TrelloBoard, TrelloBoardId, TrelloBoardName}, model::{
auth::TrelloAuth, board::TrelloBoard, list::TrelloList, TrelloBoardId, TrelloBoardName,
TrelloListId, TrelloListName,
},
}, },
}; };
@ -44,6 +49,56 @@ mod commands {
assert_eq!(result, Some(&board)); 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 🎉")
}
]
);
}
}
} }
#[test] #[test]
@ -55,7 +110,7 @@ fn trello_auth_into_hashmap() {
}; };
//when //when
let result = HashMap::<String, String>::from(&trello_auth); let result = HashMap::<String, String>::from(trello_auth);
//then //then
assert_eq!( assert_eq!(