From 4c9a0eb2c6ba5cf12d0a6ca1af05f7c6157688fe Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sat, 30 Nov 2024 18:04:48 +0000 Subject: [PATCH] feat(nextcloud): add command 'nextcloud card list' --- src/lib.rs | 15 ++ src/nextcloud/card.rs | 24 +++ src/nextcloud/mod.rs | 25 ++- src/nextcloud/model.rs | 8 +- src/nextcloud/tests.rs | 172 ++++++++++++++----- src/tests/mod.rs | 33 +--- src/tests/responses/nextcloud-card-list.json | 47 +++++ src/trello/tests.rs | 137 +++++++-------- 8 files changed, 319 insertions(+), 142 deletions(-) create mode 100644 src/nextcloud/card.rs create mode 100644 src/tests/responses/nextcloud-card-list.json diff --git a/src/lib.rs b/src/lib.rs index 288ff87..2862005 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,8 @@ enum NextcloudCommand { Board(NextcloudBoardCommand), #[clap(subcommand)] Stack(NextcloudStackCommand), + #[clap(subcommand)] + Card(NextcloudCardCommand), } #[derive(Parser, Debug)] @@ -64,6 +66,15 @@ enum NextcloudStackCommand { }, } +#[derive(Parser, Debug)] +enum NextcloudCardCommand { + List { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + stack_id: i64, + }, +} + #[derive(Parser, Debug)] enum TrelloCommand { #[clap(subcommand)] @@ -141,6 +152,10 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> { Command::Nextcloud(NextcloudCommand::Stack(NextcloudStackCommand::List { dump, })) => nextcloud::stack::list(ctx, dump).await, + Command::Nextcloud(NextcloudCommand::Card(NextcloudCardCommand::List { + dump, + stack_id, + })) => nextcloud::card::list(ctx, dump, stack_id.into()).await, } } } diff --git a/src/nextcloud/card.rs b/src/nextcloud/card.rs new file mode 100644 index 0000000..e6ed8ac --- /dev/null +++ b/src/nextcloud/card.rs @@ -0,0 +1,24 @@ +// +use crate::nextcloud::model::NextcloudStackId; +use crate::{p, FullCtx}; + +pub(crate) async fn list( + ctx: FullCtx, + dump: bool, + stack_id: NextcloudStackId, +) -> color_eyre::Result<()> { + let api_result = ctx + .deck_client() + .get_stack(ctx.cfg.nextcloud.board_id, stack_id) + .await; + if dump { + p!(ctx.prt, "{}", api_result.text); + } else { + let mut cards = api_result.result?.cards; + cards.sort_by_key(|card| card.title.clone()); + cards + .iter() + .for_each(|card| p!(ctx.prt, "{}:{}", card.id, card.title)); + } + Ok(()) +} diff --git a/src/nextcloud/mod.rs b/src/nextcloud/mod.rs index 3ee3d29..98e0134 100644 --- a/src/nextcloud/mod.rs +++ b/src/nextcloud/mod.rs @@ -2,12 +2,18 @@ use bytes::Bytes; use kxio::net::{Net, ReqBuilder}; -use crate::{api_result::APIResult, f, FullCtx}; - -use crate::nextcloud::model::{NextcloudHostname, NextcloudPassword, NextcloudUsername}; -use model::{Board, Card, NextcloudBoardId, Stack}; +use crate::{ + api_result::APIResult, + f, + nextcloud::model::{ + Board, Card, NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudStackId, + NextcloudUsername, Stack, + }, + FullCtx, +}; pub mod board; +pub mod card; pub mod model; pub mod stack; @@ -102,6 +108,17 @@ impl<'ctx> DeckClient<'ctx> { .await } + pub async fn get_stack( + &self, + board_id: NextcloudBoardId, + stack_id: NextcloudStackId, + ) -> APIResult { + self.request(f!("boards/{board_id}/stacks/{stack_id}"), |net, url| { + net.get(url) + }) + .await + } + pub async fn create_card( &self, board_id: i64, diff --git a/src/nextcloud/model.rs b/src/nextcloud/model.rs index 4849160..de58143 100644 --- a/src/nextcloud/model.rs +++ b/src/nextcloud/model.rs @@ -141,9 +141,11 @@ pub struct Stack { pub board_id: NextcloudBoardId, #[serde(rename = "ETag")] pub etag: NextcloudETag, + #[serde(default)] + pub cards: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Card { pub id: NextcloudCardId, pub title: NextcloudCardTitle, @@ -152,8 +154,10 @@ pub struct Card { pub stack_id: NextcloudStackId, pub order: NextcloudOrder, pub archived: bool, + #[serde(default)] pub due_date: Option, - pub labels: Vec, + #[serde(default)] + pub labels: Option>, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] diff --git a/src/nextcloud/tests.rs b/src/nextcloud/tests.rs index 1de68da..88e9d5a 100644 --- a/src/nextcloud/tests.rs +++ b/src/nextcloud/tests.rs @@ -1,13 +1,18 @@ // -use kxio::net::StatusCode; +use kxio::{ + fs::TempFileSystem, + net::{MockNet, StatusCode}, + print::Printer, +}; use crate::{ - config::NextcloudConfig, + config::{NextcloudConfig, TrelloConfig}, nextcloud::{ model::{ - Board, NextcloudBoardColour, NextcloudBoardId, NextcloudBoardOwner, - NextcloudBoardTitle, NextcloudETag, NextcloudHostname, NextcloudOrder, - NextcloudPassword, NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack, + Board, Card, NextcloudBoardColour, NextcloudBoardId, NextcloudBoardOwner, + NextcloudBoardTitle, NextcloudCardId, NextcloudCardTitle, NextcloudETag, + NextcloudHostname, NextcloudOrder, NextcloudPassword, NextcloudStackId, + NextcloudStackTitle, NextcloudUsername, Stack, }, DeckClient, }, @@ -124,15 +129,7 @@ mod commands { .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 ctx = given::a_full_context(mock_net, fs); let deck_client = DeckClient::new(&ctx); //when @@ -174,15 +171,7 @@ mod commands { .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 ctx = given::a_full_context(mock_net, fs); let deck_client = DeckClient::new(&ctx); //when @@ -200,32 +189,135 @@ mod commands { title: NextcloudStackTitle::new("Done"), order: NextcloudOrder::new(2), board_id: NextcloudBoardId::new(1), - etag: NextcloudETag::new("97592874d17017ef4f620c9c2a490086") + etag: NextcloudETag::new("97592874d17017ef4f620c9c2a490086"), + cards: vec![Card { + id: NextcloudCardId::new(322), + title: NextcloudCardTitle::new("Lunch: Soup & Toast"), + description: Some(s!("")), + stack_id: NextcloudStackId::new(3), + order: NextcloudOrder::new(0), + archived: false, + due_date: None, + labels: Some(vec![]) + }] }, Stack { id: NextcloudStackId::new(2), title: NextcloudStackTitle::new("Doing"), order: NextcloudOrder::new(1), board_id: NextcloudBoardId::new(1), - etag: NextcloudETag::new("3da05f904903c88450b79e4f8f6e2160") + etag: NextcloudETag::new("3da05f904903c88450b79e4f8f6e2160"), + cards: vec![ + Card { + id: NextcloudCardId::new(319), + title: NextcloudCardTitle::new("That"), + description: Some(s!("")), + stack_id: NextcloudStackId::new(2), + order: NextcloudOrder::new(0), + archived: false, + due_date: None, + labels: Some(vec![]) + }, + Card { + id: NextcloudCardId::new(323), + title: NextcloudCardTitle::new( + "Second lunch: Poached Egg & Toasted Muffin" + ), + description: Some(s!("")), + stack_id: NextcloudStackId::new(2), + order: NextcloudOrder::new(1), + archived: false, + due_date: None, + labels: Some(vec![]) + } + ] }, Stack { id: NextcloudStackId::new(1), title: NextcloudStackTitle::new("To do"), order: NextcloudOrder::new(0), board_id: NextcloudBoardId::new(1), - etag: NextcloudETag::new("b567d287210fa4d9b108ac68d5b087c1") + etag: NextcloudETag::new("b567d287210fa4d9b108ac68d5b087c1"), + cards: vec![ + Card { + id: NextcloudCardId::new(318), + title: NextcloudCardTitle::new("This"), + description: Some(s!("")), + stack_id: NextcloudStackId::new(1), + order: NextcloudOrder::new(0), + archived: false, + due_date: None, + labels: Some(vec![]) + }, + Card { + id: NextcloudCardId::new(321), + title: NextcloudCardTitle::new("Breakfast: Cereal"), + description: Some(s!("")), + stack_id: NextcloudStackId::new(1), + order: NextcloudOrder::new(1), + archived: false, + due_date: None, + labels: Some(vec![]) + } + ] } ] ); } } + + mod card { + 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/1") + .basic_auth("username", Some("password")) + .respond(StatusCode::OK) + .body(include_str!("../tests/responses/nextcloud-card-list.json")) + .expect("mock request"); + + let fs = given::a_filesystem(); + let ctx = given::a_full_context(mock_net, fs); + let deck_client = DeckClient::new(&ctx); + + //when + let result = deck_client + .get_stack(ctx.cfg.nextcloud.board_id, 1.into()) + .await + .result + .expect("get stacks"); + + assert_eq!( + result, + Stack { + id: NextcloudStackId::new(3), + title: NextcloudStackTitle::new("Done"), + order: NextcloudOrder::new(2), + board_id: NextcloudBoardId::new(1), + etag: NextcloudETag::new("dda386b3b247d7b4bd8917e19d38c01b"), + cards: vec![Card { + id: NextcloudCardId::new(322), + title: NextcloudCardTitle::new("Lunch: Soup & Toast"), + description: Some(s!("")), + stack_id: NextcloudStackId::new(3), + order: NextcloudOrder::new(0), + archived: false, + due_date: None, + labels: None + }] + } + ); + } + } } mod given { - use kxio::{fs::TempFileSystem, print::Printer}; - - use crate::{config::TrelloConfig, s}; use super::*; @@ -258,15 +350,15 @@ mod given { } } - // 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(), - // }, - // } - // } + 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(), + }, + } + } } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3cea23d..bd74cd5 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -114,6 +114,11 @@ mod template { mod given { use super::*; + use kxio::{ + fs::{FileSystem, TempFileSystem}, + net::{MockNet, Net}, + print::Printer, + }; pub fn a_context(fs: FileSystem, net: Net, prt: Printer) -> Ctx { Ctx { fs, net, prt } @@ -130,32 +135,4 @@ mod given { pub fn a_printer() -> Printer { 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, - // } - // } } diff --git a/src/tests/responses/nextcloud-card-list.json b/src/tests/responses/nextcloud-card-list.json new file mode 100644 index 0000000..412dd55 --- /dev/null +++ b/src/tests/responses/nextcloud-card-list.json @@ -0,0 +1,47 @@ +{ + "id": 3, + "title": "Done", + "boardId": 1, + "deletedAt": 0, + "lastModified": 1733515991, + "cards": [ + { + "id": 322, + "title": "Lunch: Soup & Toast", + "description": "", + "stackId": 3, + "type": "plain", + "lastModified": 1733515991, + "lastEditor": "pcampbell", + "createdAt": 1733043472, + "labels": null, + "assignedUsers": [ + { + "id": 25, + "participant": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "cardId": 322, + "type": 0 + } + ], + "attachments": null, + "attachmentCount": 0, + "owner": "pcampbell", + "order": 0, + "archived": false, + "done": null, + "duedate": null, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "ETag": "dda386b3b247d7b4bd8917e19d38c01b", + "overdue": 0 + } + ], + "order": 2, + "ETag": "dda386b3b247d7b4bd8917e19d38c01b" +} \ No newline at end of file diff --git a/src/trello/tests.rs b/src/trello/tests.rs index bc70f49..1d758f8 100644 --- a/src/trello/tests.rs +++ b/src/trello/tests.rs @@ -35,79 +35,80 @@ mod board { // //then // assert_eq!(result, Some(&board)); // } -mod commands { - mod board { - use crate::trello::{ - api::boards::TrelloBoards as _, - types::{ - TrelloBoard, TrelloBoardId, TrelloBoardName, TrelloList, TrelloListId, - TrelloListName, - }, - }; + mod commands { + mod board { + use crate::trello::{ + api::boards::TrelloBoards as _, + types::{ + TrelloBoard, TrelloBoardId, TrelloBoardName, TrelloList, TrelloListId, + TrelloListName, + }, + }; - #[test] - fn name_returns_name() { - //given - let board = TrelloBoard::new( - TrelloBoardId::new("board-id"), - TrelloBoardName::new("board-name"), - vec![], - ); - - //when - let result = board.name(); - - //then - assert_eq!(result, &TrelloBoardName::new("board-name")); - } - - #[test] - fn lists_should_return_lists() { - //given - let lists = vec![TrelloList::new( - TrelloListId::new("list-id"), - TrelloListName::new("list-name"), - )]; - let board = TrelloBoard::new( - TrelloBoardId::new("board-id"), - TrelloBoardName::new("board-name"), - lists.clone(), - ); - - //when - let result = board.lists(); - - //then - assert_eq!(result, lists); - } - - #[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"), + #[test] + fn name_returns_name() { + //given + let board = TrelloBoard::new( + TrelloBoardId::new("board-id"), + TrelloBoardName::new("board-name"), vec![], - ), - board.clone(), - TrelloBoard::new( - TrelloBoardId::new("3"), - TrelloBoardName::new("gamma"), + ); + + //when + let result = board.name(); + + //then + assert_eq!(result, &TrelloBoardName::new("board-name")); + } + + #[test] + fn lists_should_return_lists() { + //given + let lists = vec![TrelloList::new( + TrelloListId::new("list-id"), + TrelloListName::new("list-name"), + )]; + let board = TrelloBoard::new( + TrelloBoardId::new("board-id"), + TrelloBoardName::new("board-name"), + lists.clone(), + ); + + //when + let result = board.lists(); + + //then + assert_eq!(result, lists); + } + + #[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()); + //when + let result = boards.find_by_name(board.name()); - //then - assert_eq!(result, Some(&board)); + //then + assert_eq!(result, Some(&board)); + } } } }