From 584f056b455405182e1483deafef7527d8d7a6b8 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Wed, 4 Dec 2024 19:37:39 +0000 Subject: [PATCH] feat(nextcloud): add command 'nextcloud board list' --- Cargo.toml | 2 +- src/api_result.rs | 10 +- src/lib.rs | 23 ++- src/nextcloud/board.rs | 32 ++-- src/nextcloud/mod.rs | 111 ++++++------ src/nextcloud/model.rs | 20 ++- src/nextcloud/tests.rs | 158 +++++++++--------- src/tests/responses/nextcloud-board-list.json | 86 ++++++++++ 8 files changed, 272 insertions(+), 170 deletions(-) create mode 100644 src/tests/responses/nextcloud-board-list.json diff --git a/Cargo.toml b/Cargo.toml index 522925a..f6a7044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -#bytes = "1.9" +bytes = "1.9" clap = { version = "4.5", features = ["cargo", "derive"] } color-eyre = "0.6" derive_more = { version = "1.0", features = [ diff --git a/src/api_result.rs b/src/api_result.rs index a3973b2..68f2324 100644 --- a/src/api_result.rs +++ b/src/api_result.rs @@ -4,7 +4,8 @@ use kxio::{net::Response, print::Printer}; use crate::{e, s}; pub struct APIResult { - pub result: Result, + pub(crate) text: String, + pub(crate) result: Result, } impl serde::Deserialize<'a>> APIResult { @@ -20,9 +21,12 @@ impl serde::Deserialize<'a>> APIResult { e }) .map_err(kxio::net::Error::from); - Self { result } + Self { text, result } } - Err(e) => Self { result: Err(e) }, + Err(e) => Self { + text: s!(""), + result: Err(e), + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 4cde907..60952bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,22 @@ enum Command { Init, Check, Import, + #[clap(subcommand)] + Nextcloud(NextcloudCommand), +} + +#[derive(Parser, Debug)] +enum NextcloudCommand { + #[clap(subcommand)] + Board(NextcloudBoardCommand), +} + +#[derive(Parser, Debug)] +enum NextcloudBoardCommand { + List { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + }, } #[derive(Clone)] @@ -60,7 +76,7 @@ pub struct FullCtx { pub cfg: AppConfig, } impl FullCtx { - pub fn deck_client(&self) -> DeckClient { + pub(crate) fn deck_client(&self) -> DeckClient { DeckClient::new(self) } } @@ -80,7 +96,7 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> { } } Ok(cfg) => { - let _ctx = FullCtx { + let ctx = FullCtx { fs: ctx.fs, net: ctx.net, prt: ctx.prt, @@ -90,6 +106,9 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> { Command::Init => Err(eyre!("Config file already exists. Not overwriting it.")), Command::Check => todo!("check"), Command::Import => todo!("import"), + Command::Nextcloud(NextcloudCommand::Board(NextcloudBoardCommand::List { + dump, + })) => nextcloud::board::list(ctx, dump).await, } } } diff --git a/src/nextcloud/board.rs b/src/nextcloud/board.rs index de3db0b..f50994f 100644 --- a/src/nextcloud/board.rs +++ b/src/nextcloud/board.rs @@ -1,22 +1,16 @@ // -use kxio::net::Net; - -use crate::{p, AppConfig, Ctx}; use crate::{p, FullCtx}; - -use super::DeckClient; - + pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> { - let dc = DeckClient::new(&ctx.cfg.nextcloud, ctx.net); - let apiresult = dc.get_boards().await; - if dump { - p!("{}", apiresult.text); - } else { - let mut boards = apiresult.result?; - boards.sort_by_key(|stack| stack.title.clone()); - boards - .iter() - .for_each(|stack| p!("{}:{}", stack.id, stack.title)); - } - Ok(()) - } + let api_result = ctx.deck_client().get_boards().await; + if dump { + p!(ctx.prt, "{}", api_result.text); + } else { + let mut boards = api_result.result?; + boards.sort_by_key(|stack| stack.title.clone()); + boards + .iter() + .for_each(|stack| p!(ctx.prt, "{}:{}", stack.id, stack.title)); + } + Ok(()) +} diff --git a/src/nextcloud/mod.rs b/src/nextcloud/mod.rs index 3eb81f0..a03c0c8 100644 --- a/src/nextcloud/mod.rs +++ b/src/nextcloud/mod.rs @@ -1,9 +1,13 @@ // +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}; +pub mod board; pub mod model; #[cfg(test)] @@ -33,13 +37,16 @@ impl<'ctx> DeckClient<'ctx> { ) } - pub async fn get_boards(&self) -> APIResult> { + async fn request serde::Deserialize<'a>>( + &self, + url: impl Into, + custom: fn(&Net, String) -> ReqBuilder, + ) -> APIResult { APIResult::new( - self.ctx - .net - .get(self.url("boards")) + custom(&self.ctx.net, self.url(url)) .basic_auth(self.username.as_str(), Some(self.password.as_str())) .header("accept", "application/json") + .header("content-type", "application/json") .send() .await, &self.ctx.prt, @@ -47,53 +54,50 @@ impl<'ctx> DeckClient<'ctx> { .await } + async fn request_with_body serde::Deserialize<'a>>( + &self, + url: impl Into, + body: impl Into, + custom: fn(&Net, String) -> ReqBuilder, + ) -> APIResult { + APIResult::new( + custom(&self.ctx.net, self.url(url)) + .basic_auth(self.username.as_str(), Some(self.password.as_str())) + .header("accept", "application/json") + .header("content-type", "application/json") + .body(body) + .send() + .await, + &self.ctx.prt, + ) + .await + } + + pub async fn get_boards(&self) -> APIResult> { + self.request("boards", |net, url| net.get(url)).await + } + pub async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult { - APIResult::new( - self.ctx - .net - .get(self.url(f!("boards/{board_id}"))) - .basic_auth(self.username.as_str(), Some(self.password.as_str())) - .header("accept", "application/json") - .send() - .await, - &self.ctx.prt, - ) - .await + self.request(f!("boards/{board_id}"), |net, url| net.get(url)) + .await } pub async fn create_board(&self, title: &str, color: &str) -> APIResult { - APIResult::new( - self.ctx - .net - .post(self.url("boards")) - .basic_auth(self.username.as_str(), Some(self.password.as_str())) - .header("accept", "application/json") - .body( - serde_json::json!({ - "title": title, - "color": color - }) - .to_string(), - ) - .send() - .await, - &self.ctx.prt, + self.request_with_body( + "boards", + serde_json::json!({ + "title": title, + "color": color + }) + .to_string(), + |net, url| net.post(url), ) .await } pub async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult> { - APIResult::new( - self.ctx - .net - .get(self.url(f!("boards/{board_id}/stacks"))) - .basic_auth(self.username.as_str(), Some(self.password.as_str())) - .header("accept", "application/json") - .send() - .await, - &self.ctx.prt, - ) - .await + self.request(f!("boards/{board_id}/stacks"), |net, url| net.get(url)) + .await } pub async fn create_card( @@ -103,29 +107,18 @@ impl<'ctx> DeckClient<'ctx> { title: &str, description: Option<&str>, ) -> APIResult { - let url = format!( - "https://{}/index.php/apps/deck/api/v1.0/boards/{}/stacks/{}/cards", - self.hostname, board_id, stack_id - ); - - let mut json = serde_json::json!({ + let mut body = serde_json::json!({ "title": title, }); if let Some(desc) = description { - json["description"] = serde_json::Value::String(desc.to_string()); + body["description"] = serde_json::Value::String(desc.to_string()); } - APIResult::new( - self.ctx - .net - .post(&url) - .basic_auth(self.username.as_str(), Some(self.password.as_str())) - .header("accept", "application/json") - .body(json.to_string()) - .send() - .await, - &self.ctx.prt, + self.request_with_body( + format!("boards/{}/stacks/{}/cards", board_id, stack_id), + body.to_string(), + |net, url| net.post(url), ) .await } diff --git a/src/nextcloud/model.rs b/src/nextcloud/model.rs index 94fc87f..ba0e8f9 100644 --- a/src/nextcloud/model.rs +++ b/src/nextcloud/model.rs @@ -1,5 +1,5 @@ -use derive_more::derive::Display; // +use derive_more::derive::Display; use serde::{Deserialize, Serialize}; use crate::newtype; @@ -83,17 +83,11 @@ newtype!( newtype!( NextcloudBoardTitle, String, + Display, PartialOrd, Ord, "Title of the Board" ); -newtype!( - NextcloudBoardOwner, - String, - PartialOrd, - Ord, - "Owner of the Board" -); newtype!( NextcloudBoardColour, String, @@ -101,7 +95,6 @@ newtype!( Ord, "Colour of the Board" ); - newtype!( NextcloudStackTitle, String, @@ -117,6 +110,15 @@ newtype!( "Title of the Card" ); +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct NextcloudBoardOwner { + #[serde(rename = "primaryKey")] + pub primary_key: String, + pub uid: String, + #[serde(rename = "displayname")] + pub display_name: String, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Board { pub id: NextcloudBoardId, diff --git a/src/nextcloud/tests.rs b/src/nextcloud/tests.rs index a9fd2ec..4819128 100644 --- a/src/nextcloud/tests.rs +++ b/src/nextcloud/tests.rs @@ -1,10 +1,16 @@ // +use kxio::net::StatusCode; + use crate::{ config::NextcloudConfig, nextcloud::{ - model::{NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername}, + model::{ + NextcloudBoardId, NextcloudETag, NextcloudHostname, NextcloudOrder, NextcloudPassword, + NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack, + }, DeckClient, }, + AppConfig, FullCtx, }; mod config { @@ -87,86 +93,84 @@ mod config { } } -mod client { - use kxio::net::StatusCode; - use serde_json::json; - - use crate::{ - nextcloud::model::{ - Acl, Board, Label, NextcloudBoardColour, NextcloudBoardOwner, NextcloudBoardTitle, - NextcloudETag, NextcloudLabelId, NextcloudOrder, NextcloudStackId, NextcloudStackTitle, - Stack, - }, - s, - }; - +mod commands { use super::*; - #[tokio::test] - async fn get_boards() { - //given - let mock_net = kxio::net::mock(); + mod board { + use super::*; - mock_net - .on() - .get("https://host-name/index.php/apps/deck/api/v1.0/boards") - .basic_auth("username", Some("password")) - .respond(StatusCode::OK) - .body( - json!([{ - "id":2, - "title":"board-title", - "owner":"owner", - "color":"red", - "archived":false, - "labels":[ - { - "id":2, - "title":"label-title", - "color":"blue" - } - ], - "acl":[ - { - "participant":"participant", - "permission_edit":true, - "permission_share":false, - "permission_manage":true - } + 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_eq!( + 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_eq!( + output.trim(), + [ + "4:4 Published: Cossmass Infinities", + "5:Fulfilment: Cossmass Infinities", + "1:Personal Board" ] - }]) - .to_string(), - ) - .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_boards().await.result.expect("get boards"); - - assert_eq!( - result, - vec![Board { - id: NextcloudBoardId::new(2), - title: NextcloudBoardTitle::new("board-title"), - owner: NextcloudBoardOwner::new("owner"), - color: NextcloudBoardColour::new("red"), - archived: false, - labels: vec![Label { - id: NextcloudLabelId::new(2), - title: s!("label-title"), - color: s!("blue") - }], - acl: vec![Acl { - participant: s!("participant"), - permission_edit: true, - permission_share: false, - permission_manage: true - }] - }] - ); + .join("\n") + ); + } + } } #[tokio::test] diff --git a/src/tests/responses/nextcloud-board-list.json b/src/tests/responses/nextcloud-board-list.json new file mode 100644 index 0000000..29cadc4 --- /dev/null +++ b/src/tests/responses/nextcloud-board-list.json @@ -0,0 +1,86 @@ +[ + { + "id": 1, + "title": "Personal Board", + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "color": "0087C5", + "archived": false, + "labels": [], + "acl": [], + "permissions": { + "PERMISSION_READ": true, + "PERMISSION_EDIT": true, + "PERMISSION_MANAGE": true, + "PERMISSION_SHARE": true + }, + "users": [], + "shared": 0, + "stacks": [], + "activeSessions": [], + "deletedAt": 0, + "lastModified": 1733337423, + "settings": [], + "ETag": "b567d287210fa4d9b108ac68d5b087c1" + }, + { + "id": 4, + "title": "4 Published: Cossmass Infinities", + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "color": "ff0000", + "archived": true, + "labels": [], + "acl": [], + "permissions": { + "PERMISSION_READ": true, + "PERMISSION_EDIT": true, + "PERMISSION_MANAGE": true, + "PEMISSION_SHARE": true + }, + "users": [], + "shared": 0, + "stacks": [], + "activeSessions": [], + "deletedAt": 0, + "lastModified": 1699798570, + "settings": [], + "ETag": "5e0fe035f3b95672da3cba633086be37" + }, + { + "id": 5, + "title": "Fulfilment: Cossmass Infinities", + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "color": "ff0000", + "archived": true, + "labels": [], + "acl": [], + "permissions": { + "PERMISSION_READ": true, + "PERMISSION_EDIT": true, + "PERMISSION_MANAGE": true, + "PERMISSION_SHARE": true + }, + "users": [], + "shared": 0, + "stacks": [], + "activeSessions": [], + "deletedAt": 0, + "lastModified": 1699798567, + "settings": [], + "ETag": "90e2f9d53c5f6ec83088425d4486e54d" + } +]