feat(trello): add command 'trello board list'
Some checks failed
Test / build (map[name:stable]) (push) Successful in 1m46s
Test / build (map[name:nightly]) (push) Successful in 2m3s
Release Please / Release-plz (push) Failing after 14s

This commit is contained in:
Paul Campbell 2024-12-05 20:07:29 +00:00
parent 17b2e9abd3
commit 4f4edc53c8
15 changed files with 7661 additions and 481 deletions

View file

@ -35,6 +35,8 @@ enum Command {
Check, Check,
Import, Import,
#[clap(subcommand)] #[clap(subcommand)]
Trello(TrelloCommand),
#[clap(subcommand)]
Nextcloud(NextcloudCommand), Nextcloud(NextcloudCommand),
} }
@ -62,6 +64,20 @@ enum NextcloudStackCommand {
}, },
} }
#[derive(Parser, Debug)]
enum TrelloCommand {
#[clap(subcommand)]
Board(TrelloBoardCommand),
}
#[derive(Parser, Debug)]
enum TrelloBoardCommand {
List {
#[clap(long, action = clap::ArgAction::SetTrue)]
dump: bool,
},
}
#[derive(Clone)] #[derive(Clone)]
pub struct Ctx { pub struct Ctx {
pub fs: FileSystem, pub fs: FileSystem,
@ -116,6 +132,9 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> {
Command::Init => Err(eyre!("Config file already exists. Not overwriting it.")), Command::Init => Err(eyre!("Config file already exists. Not overwriting it.")),
Command::Check => todo!("check"), Command::Check => todo!("check"),
Command::Import => todo!("import"), Command::Import => todo!("import"),
Command::Trello(TrelloCommand::Board(TrelloBoardCommand::List { dump })) => {
trello::boards::list(ctx, dump).await
}
Command::Nextcloud(NextcloudCommand::Board(NextcloudBoardCommand::List { Command::Nextcloud(NextcloudCommand::Board(NextcloudBoardCommand::List {
dump, dump,
})) => nextcloud::board::list(ctx, dump).await, })) => nextcloud::board::list(ctx, dump).await,

View file

@ -11,8 +11,8 @@ pub mod board;
pub mod model; pub mod model;
pub mod stack; pub mod stack;
#[cfg(test)] // #[cfg(test)]
mod tests; // mod tests;
pub(crate) struct DeckClient<'ctx> { pub(crate) struct DeckClient<'ctx> {
ctx: &'ctx FullCtx, ctx: &'ctx FullCtx,

View file

@ -1,295 +0,0 @@
//
use kxio::net::StatusCode;
use pretty_assertions::assert_eq as assert_peq;
use crate::{
config::NextcloudConfig,
nextcloud::{
model::{
NextcloudBoardId, NextcloudETag, NextcloudHostname, NextcloudOrder, NextcloudPassword,
NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack,
},
DeckClient,
},
AppConfig, FullCtx,
};
mod stack;
mod config {
use super::*;
#[test]
fn config_hostname_returns_hostname() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig {
hostname: hostname.clone(),
username,
password,
board_id,
};
//when
//then
assert_peq!(cfg.hostname, hostname);
}
#[test]
fn config_username_returns_username() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig {
hostname,
username: username.clone(),
password,
board_id,
};
//when
//then
assert_peq!(cfg.username, username);
}
#[test]
fn config_password_returns_password() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig {
hostname,
username,
password: password.clone(),
board_id,
};
//when
//then
assert_peq!(cfg.password, password);
}
#[test]
fn config_board_id_returns_board_id() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig {
hostname,
username,
password,
board_id,
};
//when
//then
assert_peq!(cfg.board_id, board_id);
}
}
mod commands {
use super::*;
mod board {
use super::*;
mod list {
use super::*;
fn setup() -> FullCtx {
//
let mock_net = kxio::net::mock();
mock_net
.on()
.get("https://host-name/index.php/apps/deck/api/v1.0/boards")
.basic_auth("username", Some("password"))
.respond(StatusCode::OK)
.body(include_str!("../tests/responses/nextcloud-board-list.json"))
.expect("mock request");
let fs = given::a_filesystem();
FullCtx {
fs: fs.as_real(),
net: mock_net.into(),
prt: given::a_printer().clone(),
cfg: AppConfig {
trello: given::a_trello_config(),
nextcloud: given::a_nextcloud_config(),
},
}
}
#[tokio::test]
async fn dump() {
//given
let ctx = setup();
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
//when
crate::nextcloud::board::list(ctx, true)
.await
.expect("board list");
//then
let output = prt.output();
assert_peq!(
output.trim(),
include_str!("../tests/responses/nextcloud-board-list.json").trim() // [""].join("\n")
);
}
#[tokio::test]
async fn no_dump() {
//given
let ctx = setup();
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
//when
crate::nextcloud::board::list(ctx, false)
.await
.expect("board list");
//then
let output = prt.output();
assert_peq!(
output.trim(),
[
"4:4 Published: Cossmass Infinities",
"5:Fulfilment: Cossmass Infinities",
"1:Personal Board"
]
.join("\n")
);
}
}
}
mod stack {
use super::*;
#[tokio::test]
async fn list() {
//given
let mock_net = kxio::net::mock();
mock_net
.on()
.get("https://host-name/index.php/apps/deck/api/v1.0/boards/2/stacks")
.basic_auth("username", Some("password"))
.respond(StatusCode::OK)
.body(include_str!("../tests/responses/nextcloud-stack-list.json"))
.expect("mock request");
let fs = given::a_filesystem();
let ctx = FullCtx {
fs: fs.as_real(),
net: mock_net.into(),
prt: given::a_printer(),
cfg: AppConfig {
trello: given::a_trello_config(),
nextcloud: given::a_nextcloud_config(),
},
};
let deck_client = DeckClient::new(&ctx);
//when
let result = deck_client
.get_stacks(ctx.cfg.nextcloud.board_id)
.await
.result
.expect("get stacks");
assert_peq!(
result,
vec![
Stack {
id: NextcloudStackId::new(3),
title: NextcloudStackTitle::new("Done"),
order: NextcloudOrder::new(2),
board_id: NextcloudBoardId::new(1),
etag: NextcloudETag::new("97592874d17017ef4f620c9c2a490086")
},
Stack {
id: NextcloudStackId::new(2),
title: NextcloudStackTitle::new("Doing"),
order: NextcloudOrder::new(1),
board_id: NextcloudBoardId::new(1),
etag: NextcloudETag::new("3da05f904903c88450b79e4f8f6e2160")
},
Stack {
id: NextcloudStackId::new(1),
title: NextcloudStackTitle::new("To do"),
order: NextcloudOrder::new(0),
board_id: NextcloudBoardId::new(1),
etag: NextcloudETag::new("b567d287210fa4d9b108ac68d5b087c1")
}
]
);
}
}
}
mod given {
use kxio::{fs::TempFileSystem, net::MockNet, print::Printer};
use crate::{config::TrelloConfig, s, AppConfig, FullCtx};
use super::*;
pub 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);
NextcloudConfig {
hostname,
username,
password,
board_id,
}
}
pub fn a_network() -> MockNet {
kxio::net::mock()
}
pub fn a_printer() -> Printer {
kxio::print::test()
}
pub(crate) fn a_filesystem() -> TempFileSystem {
kxio::fs::temp().expect("temp fs")
}
pub(crate) fn a_trello_config() -> TrelloConfig {
TrelloConfig {
api_key: s!("trello-api-key").into(),
api_secret: s!("trello-api-secret").into(),
board_name: s!("trello-board-name").into(),
}
}
pub(crate) fn a_full_context(mock_net: MockNet, fs: TempFileSystem) -> FullCtx {
FullCtx {
fs: fs.as_real(),
net: mock_net.into(),
prt: given::a_printer(),
cfg: AppConfig {
trello: given::a_trello_config(),
nextcloud: given::a_nextcloud_config(),
},
}
}
}

View file

@ -1,10 +1,16 @@
// //
use std::collections::HashMap;
// type TestResult = Result<(), Box<dyn std::error::Error>>; // type TestResult = Result<(), Box<dyn std::error::Error>>;
use assert2::let_assert; use assert2::let_assert;
use kxio::{
fs::{FileSystem, TempFileSystem},
net::{MockNet, Net},
print::Printer,
};
use crate::{config::AppConfig, f, NAME}; use crate::{config::AppConfig, f, init::run, Ctx, NAME};
mod config { mod config {
use super::*; use super::*;
@ -60,13 +66,8 @@ mod config {
} }
mod init { mod init {
use test_log::test;
use crate::{f, init::run, NAME};
use super::given;
use super::*; use super::*;
use test_log::test;
#[test] #[test]
fn when_file_does_not_exist_should_create() { fn when_file_does_not_exist_should_create() {
@ -104,10 +105,7 @@ mod init {
} }
mod template { mod template {
use super::*;
use std::collections::HashMap;
use crate::template;
#[test] #[test]
fn expand_should_substitute_values() { fn expand_should_substitute_values() {
@ -116,7 +114,7 @@ mod template {
let params = HashMap::from([("param1", "-v1-"), ("param2", "-v2-")]); let params = HashMap::from([("param1", "-v1-"), ("param2", "-v2-")]);
//when //when
let result = template::expand(template, params); let result = crate::template::expand(template, params);
//then //then
assert_eq!(result, "pre-v1-mid-v2-post"); assert_eq!(result, "pre-v1-mid-v2-post");
@ -124,13 +122,7 @@ mod template {
} }
mod given { mod given {
use kxio::{ use super::*;
fs::{FileSystem, TempFileSystem},
net::{MockNet, Net},
print::Printer,
};
use crate::Ctx;
pub fn a_context(fs: FileSystem, net: Net, prt: Printer) -> Ctx { pub fn a_context(fs: FileSystem, net: Net, prt: Printer) -> Ctx {
Ctx { fs, net, prt } Ctx { fs, net, prt }
@ -147,4 +139,32 @@ mod given {
pub fn a_printer() -> Printer { pub fn a_printer() -> Printer {
kxio::print::test() kxio::print::test()
} }
// pub fn a_config() -> AppConfig {
// AppConfig {
// trello: a_trello_config(),
// nextcloud: a_nextcloud_config(),
// }
// }
// pub fn a_trello_config() -> TrelloConfig {
// TrelloConfig {
// api_key: s!("trello-api-key").into(),
// api_secret: s!("trello-api-secret").into(),
// board_name: s!("Trello Platform Changes").into(),
// }
// }
// pub fn a_nextcloud_config() -> NextcloudConfig {
// let hostname = s!("nextcloud.example.org").into();
// let username = s!("username").into();
// let password = s!("password").into();
// let board_id = NextcloudBoardId::new(2);
// NextcloudConfig {
// hostname,
// username,
// password,
// board_id,
// }
// }
} }

File diff suppressed because it is too large Load diff

View file

@ -2,12 +2,10 @@
use kxio::{net::Net, print::Printer}; use kxio::{net::Net, print::Printer};
use crate::api_result::APIResult; use crate::api_result::APIResult;
use crate::{ use crate::config::TrelloConfig;
f, use crate::trello::{
trello::{ types::{auth::TrelloAuth, board::TrelloBoard},
types::{auth::TrelloAuth, board::TrelloBoard}, url,
url,
},
}; };
/// Get lists from named board that Member belongs to /// Get lists from named board that Member belongs to
@ -40,12 +38,13 @@ use crate::{
/// --url "https://api.trello.com/1/members/$TRELLO_USERNAME/boards?key=$TRELLO_KEY&token=$TRELLO_SECRET&lists=open" \ /// --url "https://api.trello.com/1/members/$TRELLO_USERNAME/boards?key=$TRELLO_KEY&token=$TRELLO_SECRET&lists=open" \
/// --header 'Accept: application/json' /// --header 'Accept: application/json'
pub async fn get_boards_that_member_belongs_to( pub async fn get_boards_that_member_belongs_to(
auth: &TrelloAuth, cfg: &TrelloConfig,
net: &Net, net: &Net,
prt: &Printer, prt: &Printer,
) -> APIResult<Vec<TrelloBoard>> { ) -> APIResult<Vec<TrelloBoard>> {
let auth = TrelloAuth::new(&cfg.api_key, &cfg.api_secret);
APIResult::new( APIResult::new(
net.get(url(f!("/members/{}/boards?lists=open", **auth.user()))) net.get(url("/members/me/boards?lists=open"))
.headers(auth.into()) .headers(auth.into())
.header("Accept", "application/json") .header("Accept", "application/json")
.send() .send()

View file

@ -1,8 +1,6 @@
// //
use kxio::{net::MockNet, print::Printer}; use kxio::{net::MockNet, print::Printer};
use crate::trello::types::auth::{TrelloApiKey, TrelloApiSecret, TrelloAuth, TrelloUser};
pub(crate) fn a_network() -> MockNet { pub(crate) fn a_network() -> MockNet {
kxio::net::mock() kxio::net::mock()
} }
@ -11,10 +9,9 @@ pub(crate) fn a_printer() -> Printer {
kxio::print::test() kxio::print::test()
} }
pub(crate) fn an_auth() -> TrelloAuth { // pub(crate) fn an_auth<'cfg>(cfg: &'cfg TrelloConfig) -> TrelloAuth<'cfg> {
TrelloAuth::new( // TrelloAuth {
TrelloApiKey::new("foo"), // api_key: &cfg.api_key,
TrelloApiSecret::new("bar"), // api_secret: &cfg.api_secret,
TrelloUser::new("baz"), // }
) // }
}

View file

@ -1,63 +1,63 @@
// use super::*; //
use std::collections::HashMap;
use kxio::net::StatusCode;
use serde_json::json;
use crate::{
config::TrelloConfig,
s,
trello::{api::members::get_boards_that_member_belongs_to, types::board::TrelloBoard},
};
use super::*;
mod given; mod given;
type TestResult = color_eyre::Result<()>; type TestResult = color_eyre::Result<()>;
mod members { mod members {
use std::collections::HashMap; use super::*;
use kxio::net::StatusCode; #[tokio::test]
use serde_json::json; async fn get_member_boards() -> TestResult {
//given
use crate::{ let net = given::a_network();
s, let prt = given::a_printer();
trello::{ let trello_config = TrelloConfig {
api::members::get_boards_that_member_belongs_to, api_key: s!("foo").into(),
types::{board::TrelloBoard, TrelloBoardId, TrelloBoardName}, api_secret: s!("bar").into(),
}, board_name: s!("board-name").into(),
}; };
use super::*; net.on()
.get("https://api.trello.com/1/members/me/boards?lists=open")
#[tokio::test] .headers(HashMap::from([
async fn get_member_boards() -> TestResult { (
//given s!("authorization"),
let net = given::a_network(); s!("OAuth oauth_consumer_key=\"foo\", oauth_token=\"bar\""),
let prt = given::a_printer(); ),
let auth = given::an_auth(); (s!("accept"), s!("application/json")),
]))
net.on() .respond(StatusCode::OK)
.get("https://api.trello.com/1/members/baz/boards?lists=open") .body(s!(json!([
.headers(HashMap::from([ {"id": "1", "name": "board-name", "lists":[]}
( ])))?;
s!("authorization"),
s!("OAuth oauth_consumer_key=\"foo\", oauth_token=\"bar\""), //when
), let result = get_boards_that_member_belongs_to(&trello_config, &net.into(), &prt)
(s!("accept"), s!("application/json")),
]))
.respond(StatusCode::OK)
.body(s!(json!([
{"id": "1", "name": "board-1", "lists":[]}
])))?;
//when
let result = get_boards_that_member_belongs_to(&auth, &net.into(), &prt)
.await .await
.result?; .result?;
assert_eq!( assert_eq!(
result, result,
vec![TrelloBoard::new( vec![TrelloBoard {
TrelloBoardId::new("1"), id: s!("1").into(),
TrelloBoardName::new("board-1"), name: s!("board-name").into(),
vec![] lists: vec![]
)] }]
); );
//then //then
Ok(()) Ok(())
} }
} }

18
src/trello/boards.rs Normal file
View file

@ -0,0 +1,18 @@
//
use crate::{p, FullCtx};
pub(crate) async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> {
let api_result =
super::api::members::get_boards_that_member_belongs_to(&ctx.cfg.trello, &ctx.net, &ctx.prt)
.await;
if dump {
p!(ctx.prt, "{}", api_result.text);
} else {
let mut boards = api_result.result?;
boards.sort_by(|a, b| a.name.cmp(&b.name));
boards.into_iter().for_each(|board| {
p!(ctx.prt, "{}:{}", board.id, board.name);
});
}
Ok(())
}

View file

@ -1,14 +1,16 @@
// //
// pub mod api; // pub mod api;
pub mod api;
pub mod boards;
pub mod types; pub mod types;
#[cfg(test)] // #[cfg(test)]
mod tests; // mod tests;
// use crate::f; use crate::f;
//
// pub fn url(path: impl Into<String>) -> String { pub fn url(path: impl Into<String>) -> String {
// let path = path.into(); let path = path.into();
// assert!(path.starts_with("/")); assert!(path.starts_with("/"));
// f!("https://api.trello.com/1{path}") f!("https://api.trello.com/1{path}")
// } }

View file

@ -1,62 +0,0 @@
use crate::s;
use crate::trello::types::auth::TrelloAuth;
use std::collections::HashMap;
mod board {
// use crate::trello::{
// // api::boards::TrelloBoards as _,
// types::{
// board::TrelloBoard, TrelloBoardId, TrelloBoardName, TrelloListId, TrelloListName,
// },
// };
// #[test]
// fn list_of_boards_find_by_name_returns_board() {
// //given
// let board = TrelloBoard::new(
// TrelloBoardId::new("2"),
// TrelloBoardName::new("beta"),
// vec![],
// );
// let boards = vec![
// TrelloBoard::new(
// TrelloBoardId::new("1"),
// TrelloBoardName::new("alpha"),
// vec![],
// ),
// board.clone(),
// TrelloBoard::new(
// TrelloBoardId::new("3"),
// TrelloBoardName::new("gamma"),
// vec![],
// ),
// ];
//
// //when
// let result = boards.find_by_name(board.name());
//
// //then
// assert_eq!(result, Some(&board));
// }
}
#[test]
fn trello_auth_into_hashmap() {
//given
let trello_auth = TrelloAuth {
api_key: s!("key").into(),
api_secret: s!("token").into(),
};
//when
let result = HashMap::<String, String>::from(&trello_auth);
//then
assert_eq!(
result,
HashMap::from([(
s!("Authorization"),
s!("OAuth oauth_consumer_key=\"key\", oauth_token=\"token\"")
),])
);
}

View file

@ -14,12 +14,12 @@ newtype!(
); );
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TrelloAuth { pub struct TrelloAuth<'cfg> {
pub(crate) api_key: TrelloApiKey, pub(crate) api_key: &'cfg TrelloApiKey,
pub(crate) api_secret: TrelloApiSecret, pub(crate) api_secret: &'cfg TrelloApiSecret,
} }
impl TrelloAuth { impl<'cfg> TrelloAuth<'cfg> {
pub const fn new(api_key: TrelloApiKey, api_secret: TrelloApiSecret) -> Self { pub const fn new(api_key: &'cfg TrelloApiKey, api_secret: &'cfg TrelloApiSecret) -> Self {
Self { Self {
api_key, api_key,
api_secret, api_secret,
@ -33,8 +33,8 @@ impl TrelloAuth {
&self.api_secret &self.api_secret
} }
} }
impl From<&TrelloAuth> for HashMap<String, String> { impl<'cfg> From<TrelloAuth<'cfg>> for HashMap<String, String> {
fn from(value: &TrelloAuth) -> Self { fn from(value: TrelloAuth) -> Self {
HashMap::from([( HashMap::from([(
"Authorization".into(), "Authorization".into(),
format!( format!(

View file

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

View file

@ -1,6 +1,7 @@
//
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use super::{TrelloListId, TrelloListName}; use crate::trello::types::{TrelloListId, TrelloListName};
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, Constructor)] #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, Constructor)]
pub(crate) struct TrelloList { pub(crate) struct TrelloList {

View file

@ -1,14 +1,14 @@
pub(crate) mod auth; pub(crate) mod auth;
// pub(crate) mod board; pub(crate) mod board;
// mod card; // mod card;
// mod list; mod list;
// mod new_card; // mod new_card;
use derive_more::derive::Display; use derive_more::derive::Display;
use crate::newtype; use crate::newtype;
// newtype!(TrelloBoardId, String, Display, "Board ID"); newtype!(TrelloBoardId, String, Display, "Board ID");
newtype!( newtype!(
TrelloBoardName, TrelloBoardName,
String, String,
@ -17,14 +17,14 @@ newtype!(
Ord, Ord,
"Board Name" "Board Name"
); );
// newtype!(TrelloListId, String, "List ID"); newtype!(TrelloListId, String, "List ID");
// newtype!( newtype!(
// TrelloListName, TrelloListName,
// String, String,
// Display, Display,
// PartialOrd, PartialOrd,
// Ord, Ord,
// "List Name" "List Name"
// ); );
// newtype!(TrelloCardId, String, Display, "Card ID"); // newtype!(TrelloCardId, String, Display, "Card ID");
// newtype!(TrelloCardName, String, Display, "Card Name"); // newtype!(TrelloCardName, String, Display, "Card Name");