From 63f99c68c40bb5ffcc66849be88b510c79419f63 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' REQUEST --- Cargo.toml | 2 +- src/lib.rs | 21 ++- src/nextcloud/board.rs | 32 ++--- src/nextcloud/mod.rs | 111 +++++++-------- src/nextcloud/model.rs | 20 +-- src/nextcloud/tests.rs | 128 ++++++++---------- src/tests/responses/nextcloud-board-list.json | 86 ++++++++++++ 7 files changed, 237 insertions(+), 163 deletions(-) create mode 100644 src/tests/responses/nextcloud-board-list.json diff --git a/Cargo.toml b/Cargo.toml index c5157e3..3145df3 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/lib.rs b/src/lib.rs index 4cde907..28eecc7 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)] @@ -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 33a0ee5..6117e7b 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)] @@ -34,13 +38,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, @@ -48,53 +55,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( @@ -104,29 +108,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 5d41f20..c7fe6bb 100644 --- a/src/nextcloud/tests.rs +++ b/src/nextcloud/tests.rs @@ -1,10 +1,17 @@ // +use kxio::net::StatusCode; + use crate::{ config::NextcloudConfig, nextcloud::{ - model::{NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername}, + model::{ + Board, NextcloudBoardColour, NextcloudBoardId, NextcloudBoardOwner, + NextcloudBoardTitle, NextcloudETag, NextcloudHostname, NextcloudOrder, + NextcloudPassword, NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack, + }, DeckClient, }, + s, AppConfig, FullCtx, }; mod config { @@ -95,86 +102,59 @@ 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 { - 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 - } - ] - }]) - .to_string(), - ) - .expect("mock request"); + use super::*; - let fs = given::a_filesystem(); - let ctx = given::a_full_context(mock_net, fs); - let deck_client = DeckClient::new(&ctx); + #[tokio::test] + async fn list() { + //given + let mock_net = kxio::net::mock(); - //when - let result = deck_client.get_boards().await.result.expect("get boards"); + 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"); - 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 - }] - }] - ); + 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_boards().await.result.expect("get boards"); + + assert_eq!( + result.first(), + Some(&Board { + id: NextcloudBoardId::new(1), + title: NextcloudBoardTitle::new("Personal Board"), + owner: NextcloudBoardOwner { + primary_key: s!("pcampbell"), + uid: s!("pcampbell"), + display_name: s!("Paul Campbell"), + }, + color: NextcloudBoardColour::new("0087C5"), + archived: false, + labels: vec![], + acl: vec![] + }) + ); + } } #[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" + } +]