From be172b11a4bcf8cb76dc41b5314cdefa3d5f82dc Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 8 Dec 2024 17:23:54 +0000 Subject: [PATCH] refactor: reshuffling and extracting Executor trait --- src/api_result.rs | 2 +- src/config.rs | 30 +----- src/execute.rs | 21 +++++ src/lib.rs | 172 +++++++++-------------------------- src/nextcloud/board.rs | 21 ++++- src/nextcloud/card.rs | 79 ++++++++++++++-- src/nextcloud/client.rs | 143 +++++++++++++++++++++++++++++ src/nextcloud/mod.rs | 60 +++++++++--- src/nextcloud/model.rs | 74 +++++++-------- src/nextcloud/stack.rs | 21 ++++- src/nextcloud/tests.rs | 106 +++++++++++---------- src/tests/mod.rs | 15 ++- src/trello/api/boards.rs | 8 +- src/trello/api/cards/mod.rs | 7 +- src/trello/api/lists.rs | 2 +- src/trello/api/members.rs | 19 ++-- src/trello/api/mod.rs | 8 +- src/trello/api/tests/mod.rs | 3 +- src/trello/boards.rs | 51 +++++++---- src/trello/client.rs | 22 +++++ src/trello/mod.rs | 38 ++++++-- src/trello/tests.rs | 18 ++-- src/trello/types/auth.rs | 18 +--- src/trello/types/card.rs | 6 +- src/trello/types/mod.rs | 2 +- src/trello/types/new_card.rs | 8 +- 26 files changed, 599 insertions(+), 355 deletions(-) create mode 100644 src/execute.rs create mode 100644 src/nextcloud/client.rs create mode 100644 src/trello/client.rs diff --git a/src/api_result.rs b/src/api_result.rs index 68f2324..bf9ca53 100644 --- a/src/api_result.rs +++ b/src/api_result.rs @@ -3,7 +3,7 @@ use kxio::{net::Response, print::Printer}; use crate::{e, s}; -pub struct APIResult { +pub(crate) struct APIResult { pub(crate) text: String, pub(crate) result: Result, } diff --git a/src/config.rs b/src/config.rs index 1824515..07ae72e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,41 +1,17 @@ // use color_eyre::Result; -use crate::{ - f, - nextcloud::model::{NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername}, - s, - trello::types::{ - auth::{TrelloApiKey, TrelloApiSecret}, - TrelloBoardName, - }, - Ctx, NAME, -}; - -#[derive(Clone, Debug, derive_more::From, PartialEq, Eq, serde::Deserialize)] -pub(crate) struct TrelloConfig { - pub(crate) api_key: TrelloApiKey, - pub(crate) api_secret: TrelloApiSecret, - pub(crate) board_name: TrelloBoardName, -} - -#[derive(Clone, Debug, derive_more::From, PartialEq, Eq, serde::Deserialize)] -pub(crate) struct NextcloudConfig { - pub(crate) hostname: NextcloudHostname, - pub(crate) username: NextcloudUsername, - pub(crate) password: NextcloudPassword, - pub(crate) board_id: NextcloudBoardId, -} +use crate::{f, nextcloud::NextcloudConfig, s, trello::TrelloConfig, Ctx, NAME}; #[derive( Clone, Debug, derive_more::From, PartialEq, Eq, derive_more::AsRef, serde::Deserialize, )] -pub struct AppConfig { +pub(crate) struct AppConfig { pub(crate) trello: TrelloConfig, pub(crate) nextcloud: NextcloudConfig, } impl AppConfig { - pub fn load(ctx: &Ctx) -> Result { + pub(crate) fn load(ctx: &Ctx) -> Result { let file = ctx.fs.base().join(f!("{NAME}.toml")); let str = ctx.fs.file(&file).reader()?; Ok(toml::from_str(s!(str).as_str())?) diff --git a/src/execute.rs b/src/execute.rs new file mode 100644 index 0000000..401ab11 --- /dev/null +++ b/src/execute.rs @@ -0,0 +1,21 @@ +// +use color_eyre::eyre::eyre; +use color_eyre::Result; + +use crate::FullCtx; + +pub(crate) trait Execute { + async fn execute(self, ctx: FullCtx) -> Result<()>; +} + +impl Execute for crate::Command { + async fn execute(self, ctx: FullCtx) -> Result<()> { + match self { + Self::Init => Err(eyre!("Config file already exists. Not overwriting it.")), + Self::Check => crate::check::run(ctx).await, + Self::Import => todo!(), //crate::import::run(ctx).await, + Self::Trello(cmd) => cmd.execute(ctx).await, + Self::Nextcloud(cmd) => cmd.execute(ctx).await, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 39c6f61..b49aac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,26 +3,30 @@ use std::path::PathBuf; use clap::Parser; use color_eyre::eyre::eyre; -pub use config::AppConfig; +use config::AppConfig; use kxio::{fs::FileSystem, net::Net, print::Printer}; mod api_result; mod check; mod config; +mod execute; mod init; mod macros; -pub mod nextcloud; +mod nextcloud; mod template; mod trello; #[cfg(test)] mod tests; -pub const NAME: &str = "trello-to-deck"; +const NAME: &str = "trello-to-deck"; -pub use kxio::kxeprintln as e; -pub use kxio::kxprintln as p; -use nextcloud::DeckClient; +use crate::nextcloud::client::DeckClient; +use crate::trello::client::TrelloClient; +use execute::Execute; + +use kxio::kxeprintln as e; +use kxio::kxprintln as p; #[derive(Parser, Debug)] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] @@ -34,77 +38,27 @@ struct Commands { } #[derive(Parser, Debug)] enum Command { + /// Initialize the configuration + #[command(about = "Initialize configuration")] Init, + + /// Check the configuration and connection + #[command(about = "Check configuration and connection")] Check, + + /// Import boards from Trello to Nextcloud Deck + #[command(about = "Import boards from Trello to Nextcloud Deck")] Import, - #[clap(subcommand)] - Trello(TrelloCommand), - #[clap(subcommand)] - Nextcloud(NextcloudCommand), -} -#[derive(Parser, Debug)] -enum NextcloudCommand { + /// Trello-specific commands + #[command(about = "Trello-specific commands")] #[clap(subcommand)] - Board(NextcloudBoardCommand), + Trello(trello::TrelloCommand), + + /// Nextcloud-specific commands + #[command(about = "Nextcloud-specific commands")] #[clap(subcommand)] - Stack(NextcloudStackCommand), - #[clap(subcommand)] - Card(NextcloudCardCommand), -} - -#[derive(Parser, Debug)] -enum NextcloudBoardCommand { - List { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - }, -} - -#[derive(Parser, Debug)] -enum NextcloudStackCommand { - List { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - }, -} - -#[derive(Parser, Debug)] -enum NextcloudCardCommand { - List { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - stack_id: i64, - }, - Get { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - stack_id: i64, - card_id: i64, - }, - Create { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - stack_id: i64, - #[clap(long)] - title: String, - #[clap(long)] - description: Option, - }, -} - -#[derive(Parser, Debug)] -enum TrelloCommand { - #[clap(subcommand)] - Board(TrelloBoardCommand), -} - -#[derive(Parser, Debug)] -enum TrelloBoardCommand { - List { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - }, + Nextcloud(nextcloud::NextcloudCommand), } #[derive(Clone)] @@ -124,16 +78,20 @@ impl Default for Ctx { } #[derive(Clone)] -pub struct FullCtx { - pub fs: FileSystem, +pub(crate) struct FullCtx { + // pub fs: FileSystem, pub net: Net, pub prt: Printer, pub cfg: AppConfig, } impl FullCtx { - pub fn deck_client(&self) -> DeckClient { + pub(crate) fn deck_client(&self) -> DeckClient { DeckClient::new(self) } + + pub(crate) fn trello_client(&self) -> TrelloClient { + TrelloClient::new(self) + } } #[cfg_attr(test, mutants::skip)] @@ -160,63 +118,15 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> { } } Ok(cfg) => { - let ctx = FullCtx { - fs: ctx.fs, - net: ctx.net, - prt: ctx.prt, - cfg, - }; - match commands.command { - Command::Init => Err(eyre!("Config file already exists. Not overwriting it.")), - Command::Check => check::run(ctx).await, - Command::Import => todo!("import"), - Command::Trello(trello) => match trello { - TrelloCommand::Board(board) => match board { - TrelloBoardCommand::List { dump } => { - nextcloud::board::list(ctx, dump).await - } - }, - }, - Command::Nextcloud(nextcloud) => match nextcloud { - NextcloudCommand::Board(board) => match board { - NextcloudBoardCommand::List { dump } => { - nextcloud::board::list(ctx, dump).await - } - }, - NextcloudCommand::Stack(stack) => match stack { - NextcloudStackCommand::List { dump } => { - nextcloud::stack::list(ctx, dump).await - } - }, - NextcloudCommand::Card(card) => match card { - NextcloudCardCommand::List { dump, stack_id } => { - nextcloud::card::list(ctx, dump, stack_id.into()).await - } - NextcloudCardCommand::Get { - dump, - stack_id, - card_id, - } => nextcloud::card::get(ctx, dump, stack_id.into(), card_id.into()).await, - NextcloudCardCommand::Create { - dump, - stack_id, - title, - description, - } => { - nextcloud::card::create( - ctx, - nextcloud::card::Create { - dump, - stack_id, - title, - description, - }, - ) - .await - } - }, - }, - } + commands + .command + .execute(FullCtx { + // fs: ctx.fs, + net: ctx.net, + prt: ctx.prt, + cfg, + }) + .await } } } diff --git a/src/nextcloud/board.rs b/src/nextcloud/board.rs index f50994f..4a234b2 100644 --- a/src/nextcloud/board.rs +++ b/src/nextcloud/board.rs @@ -1,7 +1,26 @@ // +use clap::Parser; + +use crate::execute::Execute; use crate::{p, FullCtx}; -pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> { +#[derive(Parser, Debug)] +pub(crate) enum NextcloudBoardCommand { + List { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + }, +} + +impl Execute for NextcloudBoardCommand { + async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { + match self { + Self::List { dump } => list(&ctx, dump).await, + } + } +} + +pub(crate) async fn list(ctx: &FullCtx, dump: bool) -> color_eyre::Result<()> { let api_result = ctx.deck_client().get_boards().await; if dump { p!(ctx.prt, "{}", api_result.text); diff --git a/src/nextcloud/card.rs b/src/nextcloud/card.rs index 67e0365..eb00021 100644 --- a/src/nextcloud/card.rs +++ b/src/nextcloud/card.rs @@ -1,11 +1,78 @@ // +use clap::Parser; + +use crate::execute::Execute; use crate::{ nextcloud::model::{NextcloudCardId, NextcloudStackId}, p, FullCtx, }; +#[derive(Parser, Debug)] +pub(crate) enum NextcloudCardCommand { + List { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + stack_id: i64, + }, + Get { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + stack_id: i64, + card_id: i64, + }, + Create { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + stack_id: i64, + #[clap(long)] + title: String, + #[clap(long)] + description: Option, + }, +} + +impl Execute for NextcloudCardCommand { + async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { + match self { + Self::List { dump, stack_id } => { + list(&ctx, dump, NextcloudStackId::from(stack_id)).await + } + Self::Get { + dump, + stack_id, + card_id, + } => { + get( + &ctx, + dump, + NextcloudStackId::from(stack_id), + NextcloudCardId::from(card_id), + ) + .await + } + Self::Create { + dump, + stack_id, + title, + description, + } => { + create( + ctx, + Create { + dump, + stack_id, + title, + description, + }, + ) + .await + } + } + } +} + pub(crate) async fn list( - ctx: FullCtx, + ctx: &FullCtx, dump: bool, stack_id: NextcloudStackId, ) -> color_eyre::Result<()> { @@ -26,7 +93,7 @@ pub(crate) async fn list( } pub(crate) async fn get( - ctx: FullCtx, + ctx: &FullCtx, dump: bool, stack_id: NextcloudStackId, card_id: NextcloudCardId, @@ -45,10 +112,10 @@ pub(crate) async fn get( } pub(crate) struct Create { - pub dump: bool, - pub stack_id: i64, - pub title: String, - pub description: Option, + pub(crate) dump: bool, + pub(crate) stack_id: i64, + pub(crate) title: String, + pub(crate) description: Option, } pub(crate) async fn create(ctx: FullCtx, create: Create) -> color_eyre::Result<()> { let dc = ctx.deck_client(); diff --git a/src/nextcloud/client.rs b/src/nextcloud/client.rs new file mode 100644 index 0000000..2a4a361 --- /dev/null +++ b/src/nextcloud/client.rs @@ -0,0 +1,143 @@ +use crate::api_result::APIResult; +use crate::nextcloud::card::Create; +use crate::nextcloud::model::{ + Board, Card, NextcloudBoardId, NextcloudCardId, NextcloudHostname, NextcloudPassword, + NextcloudStackId, NextcloudUsername, Stack, +}; +use crate::{f, FullCtx}; +use bytes::Bytes; +use kxio::net::{Net, ReqBuilder}; + +pub(crate) struct DeckClient<'ctx> { + ctx: &'ctx FullCtx, + hostname: &'ctx NextcloudHostname, + username: &'ctx NextcloudUsername, + password: &'ctx NextcloudPassword, +} + +// Uses the API described here: https://deck.readthedocs.io/en/stable/API/#cards +impl<'ctx> DeckClient<'ctx> { + pub fn new(ctx: &'ctx FullCtx) -> Self { + Self { + ctx, + hostname: &ctx.cfg.nextcloud.hostname, + username: &ctx.cfg.nextcloud.username, + password: &ctx.cfg.nextcloud.password, + } + } + + fn url(&self, path: impl Into) -> String { + f!( + "https://{}/index.php/apps/deck/api/v1.0/{}", + self.hostname, + path.into() + ) + } + + async fn request serde::Deserialize<'a>>( + &self, + url: 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") + .send() + .await, + &self.ctx.prt, + ) + .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(crate) async fn get_boards(&self) -> APIResult> { + self.request("boards", |net, url| net.get(url)).await + } + + pub(crate) async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult { + self.request(f!("boards/{board_id}"), |net, url| net.get(url)) + .await + } + + // pub(crate) async fn create_board(&self, title: &str, color: &str) -> APIResult { + // self.request_with_body( + // "boards", + // serde_json::json!({ + // "title": title, + // "color": color + // }) + // .to_string(), + // |net, url| net.post(url), + // ) + // .await + // } + + pub(crate) async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult> { + self.request(f!("boards/{board_id}/stacks"), |net, url| net.get(url)) + .await + } + + pub(crate) 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(crate) async fn create_card(&self, create: &Create) -> APIResult { + let mut body = serde_json::json!({ + "title": create.title, + }); + + if let Some(desc) = &create.description { + body["description"] = serde_json::Value::String(desc.to_string()); + } + + self.request_with_body( + format!( + "boards/{}/stacks/{}/cards", + self.ctx.cfg.nextcloud.board_id, create.stack_id + ), + body.to_string(), + |net, url| net.post(url), + ) + .await + } + + pub(crate) async fn get_card( + &self, + board_id: NextcloudBoardId, + stack_id: NextcloudStackId, + card_id: NextcloudCardId, + ) -> APIResult { + self.request( + f!("boards/{board_id}/stacks/{stack_id}/cards/{card_id}"), + |net, url| net.get(url), + ) + .await + } +} diff --git a/src/nextcloud/mod.rs b/src/nextcloud/mod.rs index e08d652..0b95c63 100644 --- a/src/nextcloud/mod.rs +++ b/src/nextcloud/mod.rs @@ -1,19 +1,30 @@ -// use bytes::Bytes; +// +use clap::Parser; use kxio::net::{Net, ReqBuilder}; -use crate::{api_result::APIResult, f, FullCtx}; - -use card::Create; -use model::{ - Board, Card, NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudStackId, - NextcloudUsername, Stack, +use crate::{ + api_result::APIResult, + execute::Execute, + f, + nextcloud::{ + board::NextcloudBoardCommand, + card::Create, + card::NextcloudCardCommand, + model::{ + Board, Card, NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudStackId, + NextcloudUsername, Stack, + }, + stack::NextcloudStackCommand, + }, + FullCtx, }; -pub mod board; -pub mod card; -pub mod model; -pub mod stack; +mod board; +mod card; +pub(crate) mod client; +pub(crate) mod model; +mod stack; #[cfg(test)] mod tests; @@ -151,3 +162,30 @@ impl<'ctx> DeckClient<'ctx> { .await } } + +#[derive(Parser, Debug)] +pub(crate) enum NextcloudCommand { + #[clap(subcommand)] + Board(NextcloudBoardCommand), + #[clap(subcommand)] + Stack(NextcloudStackCommand), + #[clap(subcommand)] + Card(NextcloudCardCommand), +} +impl Execute for NextcloudCommand { + async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { + match self { + NextcloudCommand::Board(cmd) => cmd.execute(ctx).await, + NextcloudCommand::Stack(cmd) => cmd.execute(ctx).await, + NextcloudCommand::Card(cmd) => cmd.execute(ctx).await, + } + } +} + +#[derive(Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)] +pub struct NextcloudConfig { + pub(crate) hostname: NextcloudHostname, + pub(crate) username: NextcloudUsername, + pub(crate) password: NextcloudPassword, + pub(crate) board_id: NextcloudBoardId, +} diff --git a/src/nextcloud/model.rs b/src/nextcloud/model.rs index 0367a00..991cb55 100644 --- a/src/nextcloud/model.rs +++ b/src/nextcloud/model.rs @@ -113,64 +113,64 @@ newtype!( ); #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct NextcloudBoardOwner { +pub(crate) struct NextcloudBoardOwner { #[serde(rename = "primaryKey")] - pub primary_key: String, - pub uid: String, + pub(crate) primary_key: String, + pub(crate) uid: String, #[serde(rename = "displayname")] - pub display_name: String, + pub(crate) display_name: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct Board { - pub id: NextcloudBoardId, - pub title: NextcloudBoardTitle, - pub owner: NextcloudBoardOwner, - pub color: NextcloudBoardColour, - pub archived: bool, - pub labels: Vec