feat(nextcloud): add command 'nextcloud board list'

This commit is contained in:
Paul Campbell 2024-12-04 19:37:39 +00:00
parent fdd623a7e4
commit ffcb07de36
8 changed files with 244 additions and 166 deletions

View file

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
#bytes = "1.9" bytes = "1.9"
clap = { version = "4.5", features = ["cargo", "derive"] } clap = { version = "4.5", features = ["cargo", "derive"] }
color-eyre = "0.6" color-eyre = "0.6"
derive_more = { version = "1.0", features = [ derive_more = { version = "1.0", features = [

View file

@ -4,7 +4,8 @@ use kxio::{net::Response, print::Printer};
use crate::{e, s}; use crate::{e, s};
pub struct APIResult<T> { pub struct APIResult<T> {
pub result: Result<T, kxio::net::Error>, pub(crate) text: String,
pub(crate) result: Result<T, kxio::net::Error>,
} }
impl<T: for<'a> serde::Deserialize<'a>> APIResult<T> { impl<T: for<'a> serde::Deserialize<'a>> APIResult<T> {
@ -20,9 +21,12 @@ impl<T: for<'a> serde::Deserialize<'a>> APIResult<T> {
e e
}) })
.map_err(kxio::net::Error::from); .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),
},
} }
} }
} }

View file

@ -34,6 +34,22 @@ enum Command {
Init, Init,
Check, Check,
Import, 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)] #[derive(Clone)]
@ -80,7 +96,7 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> {
} }
} }
Ok(cfg) => { Ok(cfg) => {
let _ctx = FullCtx { let ctx = FullCtx {
fs: ctx.fs, fs: ctx.fs,
net: ctx.net, net: ctx.net,
prt: ctx.prt, 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::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::Nextcloud(NextcloudCommand::Board(NextcloudBoardCommand::List {
dump,
})) => nextcloud::board::list(ctx, dump).await,
} }
} }
} }

View file

@ -1,22 +1,16 @@
// //
use kxio::net::Net;
use crate::{p, AppConfig, Ctx};
use crate::{p, FullCtx}; use crate::{p, FullCtx};
use super::DeckClient;
pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> { pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> {
let dc = DeckClient::new(&ctx.cfg.nextcloud, ctx.net); let api_result = ctx.deck_client().get_boards().await;
let apiresult = dc.get_boards().await; if dump {
if dump { p!(ctx.prt, "{}", api_result.text);
p!("{}", apiresult.text); } else {
} else { let mut boards = api_result.result?;
let mut boards = apiresult.result?; boards.sort_by_key(|stack| stack.title.clone());
boards.sort_by_key(|stack| stack.title.clone()); boards
boards .iter()
.iter() .for_each(|stack| p!(ctx.prt, "{}:{}", stack.id, stack.title));
.for_each(|stack| p!("{}:{}", stack.id, stack.title)); }
} Ok(())
Ok(()) }
}

View file

@ -1,9 +1,13 @@
// //
use bytes::Bytes;
use kxio::net::{Net, ReqBuilder};
use crate::{api_result::APIResult, f, FullCtx}; use crate::{api_result::APIResult, f, FullCtx};
use crate::nextcloud::model::{NextcloudHostname, NextcloudPassword, NextcloudUsername}; use crate::nextcloud::model::{NextcloudHostname, NextcloudPassword, NextcloudUsername};
use model::{Board, Card, NextcloudBoardId, Stack}; use model::{Board, Card, NextcloudBoardId, Stack};
pub mod board;
pub mod model; pub mod model;
#[cfg(test)] #[cfg(test)]
@ -33,13 +37,16 @@ impl<'ctx> DeckClient<'ctx> {
) )
} }
pub async fn get_boards(&self) -> APIResult<Vec<Board>> { async fn request<T: for<'a> serde::Deserialize<'a>>(
&self,
url: impl Into<String>,
custom: fn(&Net, String) -> ReqBuilder,
) -> APIResult<T> {
APIResult::new( APIResult::new(
self.ctx custom(&self.ctx.net, self.url(url))
.net
.get(self.url("boards"))
.basic_auth(self.username.as_str(), Some(self.password.as_str())) .basic_auth(self.username.as_str(), Some(self.password.as_str()))
.header("accept", "application/json") .header("accept", "application/json")
.header("content-type", "application/json")
.send() .send()
.await, .await,
&self.ctx.prt, &self.ctx.prt,
@ -47,53 +54,50 @@ impl<'ctx> DeckClient<'ctx> {
.await .await
} }
async fn request_with_body<T: for<'a> serde::Deserialize<'a>>(
&self,
url: impl Into<String>,
body: impl Into<Bytes>,
custom: fn(&Net, String) -> ReqBuilder,
) -> APIResult<T> {
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<Vec<Board>> {
self.request("boards", |net, url| net.get(url)).await
}
pub async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult<Board> { pub async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult<Board> {
APIResult::new( self.request(f!("boards/{board_id}"), |net, url| net.get(url))
self.ctx .await
.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
} }
pub async fn create_board(&self, title: &str, color: &str) -> APIResult<Board> { pub async fn create_board(&self, title: &str, color: &str) -> APIResult<Board> {
APIResult::new( self.request_with_body(
self.ctx "boards",
.net serde_json::json!({
.post(self.url("boards")) "title": title,
.basic_auth(self.username.as_str(), Some(self.password.as_str())) "color": color
.header("accept", "application/json") })
.body( .to_string(),
serde_json::json!({ |net, url| net.post(url),
"title": title,
"color": color
})
.to_string(),
)
.send()
.await,
&self.ctx.prt,
) )
.await .await
} }
pub async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult<Vec<Stack>> { pub async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult<Vec<Stack>> {
APIResult::new( self.request(f!("boards/{board_id}/stacks"), |net, url| net.get(url))
self.ctx .await
.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
} }
pub async fn create_card( pub async fn create_card(
@ -103,29 +107,18 @@ impl<'ctx> DeckClient<'ctx> {
title: &str, title: &str,
description: Option<&str>, description: Option<&str>,
) -> APIResult<Card> { ) -> APIResult<Card> {
let url = format!( let mut body = serde_json::json!({
"https://{}/index.php/apps/deck/api/v1.0/boards/{}/stacks/{}/cards",
self.hostname, board_id, stack_id
);
let mut json = serde_json::json!({
"title": title, "title": title,
}); });
if let Some(desc) = description { 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.request_with_body(
self.ctx format!("boards/{}/stacks/{}/cards", board_id, stack_id),
.net body.to_string(),
.post(&url) |net, url| 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,
) )
.await .await
} }

View file

@ -1,5 +1,5 @@
use derive_more::derive::Display;
// //
use derive_more::derive::Display;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::newtype; use crate::newtype;
@ -83,17 +83,11 @@ newtype!(
newtype!( newtype!(
NextcloudBoardTitle, NextcloudBoardTitle,
String, String,
Display,
PartialOrd, PartialOrd,
Ord, Ord,
"Title of the Board" "Title of the Board"
); );
newtype!(
NextcloudBoardOwner,
String,
PartialOrd,
Ord,
"Owner of the Board"
);
newtype!( newtype!(
NextcloudBoardColour, NextcloudBoardColour,
String, String,
@ -101,7 +95,6 @@ newtype!(
Ord, Ord,
"Colour of the Board" "Colour of the Board"
); );
newtype!( newtype!(
NextcloudStackTitle, NextcloudStackTitle,
String, String,
@ -117,6 +110,15 @@ newtype!(
"Title of the Card" "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)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Board { pub struct Board {
pub id: NextcloudBoardId, pub id: NextcloudBoardId,

View file

@ -1,10 +1,17 @@
// //
use kxio::net::StatusCode;
use crate::{ use crate::{
config::NextcloudConfig, config::NextcloudConfig,
nextcloud::{ nextcloud::{
model::{NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername}, model::{
Board, NextcloudBoardColour, NextcloudBoardId, NextcloudBoardOwner,
NextcloudBoardTitle, NextcloudETag, NextcloudHostname, NextcloudOrder,
NextcloudPassword, NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack,
},
DeckClient, DeckClient,
}, },
s, AppConfig, FullCtx,
}; };
mod config { mod config {
@ -87,86 +94,59 @@ mod config {
} }
} }
mod client { mod commands {
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,
};
use super::*; use super::*;
#[tokio::test] mod board {
async fn get_boards() {
//given
let mock_net = kxio::net::mock();
mock_net use super::*;
.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");
let fs = given::a_filesystem(); #[tokio::test]
let ctx = given::a_full_context(mock_net, fs); async fn list() {
let deck_client = DeckClient::new(&ctx); //given
let mock_net = kxio::net::mock();
//when mock_net
let result = deck_client.get_boards().await.result.expect("get boards"); .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!( let fs = given::a_filesystem();
result, let ctx = FullCtx {
vec![Board { fs: fs.as_real(),
id: NextcloudBoardId::new(2), net: mock_net.into(),
title: NextcloudBoardTitle::new("board-title"), prt: given::a_printer(),
owner: NextcloudBoardOwner::new("owner"), cfg: AppConfig {
color: NextcloudBoardColour::new("red"), trello: given::a_trello_config(),
archived: false, nextcloud: given::a_nextcloud_config(),
labels: vec![Label { },
id: NextcloudLabelId::new(2), };
title: s!("label-title"), let deck_client = DeckClient::new(&ctx);
color: s!("blue")
}], //when
acl: vec![Acl { let result = deck_client.get_boards().await.result.expect("get boards");
participant: s!("participant"),
permission_edit: true, assert_eq!(
permission_share: false, result.first(),
permission_manage: true 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] #[tokio::test]

View file

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