refactor: reshuffling and extracting Executor trait

This commit is contained in:
Paul Campbell 2024-12-08 17:23:54 +00:00
parent 3f0ecd5fd1
commit be172b11a4
26 changed files with 599 additions and 355 deletions

View file

@ -3,7 +3,7 @@ use kxio::{net::Response, print::Printer};
use crate::{e, s}; use crate::{e, s};
pub struct APIResult<T> { pub(crate) struct APIResult<T> {
pub(crate) text: String, pub(crate) text: String,
pub(crate) result: Result<T, kxio::net::Error>, pub(crate) result: Result<T, kxio::net::Error>,
} }

View file

@ -1,41 +1,17 @@
// //
use color_eyre::Result; use color_eyre::Result;
use crate::{ use crate::{f, nextcloud::NextcloudConfig, s, trello::TrelloConfig, Ctx, NAME};
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,
}
#[derive( #[derive(
Clone, Debug, derive_more::From, PartialEq, Eq, derive_more::AsRef, serde::Deserialize, 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) trello: TrelloConfig,
pub(crate) nextcloud: NextcloudConfig, pub(crate) nextcloud: NextcloudConfig,
} }
impl AppConfig { impl AppConfig {
pub fn load(ctx: &Ctx) -> Result<Self> { pub(crate) fn load(ctx: &Ctx) -> Result<Self> {
let file = ctx.fs.base().join(f!("{NAME}.toml")); let file = ctx.fs.base().join(f!("{NAME}.toml"));
let str = ctx.fs.file(&file).reader()?; let str = ctx.fs.file(&file).reader()?;
Ok(toml::from_str(s!(str).as_str())?) Ok(toml::from_str(s!(str).as_str())?)

21
src/execute.rs Normal file
View file

@ -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,
}
}
}

View file

@ -3,26 +3,30 @@ use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
pub use config::AppConfig; use config::AppConfig;
use kxio::{fs::FileSystem, net::Net, print::Printer}; use kxio::{fs::FileSystem, net::Net, print::Printer};
mod api_result; mod api_result;
mod check; mod check;
mod config; mod config;
mod execute;
mod init; mod init;
mod macros; mod macros;
pub mod nextcloud; mod nextcloud;
mod template; mod template;
mod trello; mod trello;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub const NAME: &str = "trello-to-deck"; const NAME: &str = "trello-to-deck";
pub use kxio::kxeprintln as e; use crate::nextcloud::client::DeckClient;
pub use kxio::kxprintln as p; use crate::trello::client::TrelloClient;
use nextcloud::DeckClient; use execute::Execute;
use kxio::kxeprintln as e;
use kxio::kxprintln as p;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
@ -34,77 +38,27 @@ struct Commands {
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
enum Command { enum Command {
/// Initialize the configuration
#[command(about = "Initialize configuration")]
Init, Init,
/// Check the configuration and connection
#[command(about = "Check configuration and connection")]
Check, Check,
/// Import boards from Trello to Nextcloud Deck
#[command(about = "Import boards from Trello to Nextcloud Deck")]
Import, Import,
#[clap(subcommand)]
Trello(TrelloCommand),
#[clap(subcommand)]
Nextcloud(NextcloudCommand),
}
#[derive(Parser, Debug)] /// Trello-specific commands
enum NextcloudCommand { #[command(about = "Trello-specific commands")]
#[clap(subcommand)] #[clap(subcommand)]
Board(NextcloudBoardCommand), Trello(trello::TrelloCommand),
/// Nextcloud-specific commands
#[command(about = "Nextcloud-specific commands")]
#[clap(subcommand)] #[clap(subcommand)]
Stack(NextcloudStackCommand), Nextcloud(nextcloud::NextcloudCommand),
#[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<String>,
},
}
#[derive(Parser, Debug)]
enum TrelloCommand {
#[clap(subcommand)]
Board(TrelloBoardCommand),
}
#[derive(Parser, Debug)]
enum TrelloBoardCommand {
List {
#[clap(long, action = clap::ArgAction::SetTrue)]
dump: bool,
},
} }
#[derive(Clone)] #[derive(Clone)]
@ -124,16 +78,20 @@ impl Default for Ctx {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct FullCtx { pub(crate) struct FullCtx {
pub fs: FileSystem, // pub fs: FileSystem,
pub net: Net, pub net: Net,
pub prt: Printer, pub prt: Printer,
pub cfg: AppConfig, pub cfg: AppConfig,
} }
impl FullCtx { impl FullCtx {
pub fn deck_client(&self) -> DeckClient { pub(crate) fn deck_client(&self) -> DeckClient {
DeckClient::new(self) DeckClient::new(self)
} }
pub(crate) fn trello_client(&self) -> TrelloClient {
TrelloClient::new(self)
}
} }
#[cfg_attr(test, mutants::skip)] #[cfg_attr(test, mutants::skip)]
@ -160,63 +118,15 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> {
} }
} }
Ok(cfg) => { Ok(cfg) => {
let ctx = FullCtx { commands
fs: ctx.fs, .command
.execute(FullCtx {
// fs: ctx.fs,
net: ctx.net, net: ctx.net,
prt: ctx.prt, prt: ctx.prt,
cfg, 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 .await
} }
},
},
}
}
} }
} }

View file

@ -1,7 +1,26 @@
// //
use clap::Parser;
use crate::execute::Execute;
use crate::{p, FullCtx}; 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; let api_result = ctx.deck_client().get_boards().await;
if dump { if dump {
p!(ctx.prt, "{}", api_result.text); p!(ctx.prt, "{}", api_result.text);

View file

@ -1,11 +1,78 @@
// //
use clap::Parser;
use crate::execute::Execute;
use crate::{ use crate::{
nextcloud::model::{NextcloudCardId, NextcloudStackId}, nextcloud::model::{NextcloudCardId, NextcloudStackId},
p, FullCtx, 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<String>,
},
}
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( pub(crate) async fn list(
ctx: FullCtx, ctx: &FullCtx,
dump: bool, dump: bool,
stack_id: NextcloudStackId, stack_id: NextcloudStackId,
) -> color_eyre::Result<()> { ) -> color_eyre::Result<()> {
@ -26,7 +93,7 @@ pub(crate) async fn list(
} }
pub(crate) async fn get( pub(crate) async fn get(
ctx: FullCtx, ctx: &FullCtx,
dump: bool, dump: bool,
stack_id: NextcloudStackId, stack_id: NextcloudStackId,
card_id: NextcloudCardId, card_id: NextcloudCardId,
@ -45,10 +112,10 @@ pub(crate) async fn get(
} }
pub(crate) struct Create { pub(crate) struct Create {
pub dump: bool, pub(crate) dump: bool,
pub stack_id: i64, pub(crate) stack_id: i64,
pub title: String, pub(crate) title: String,
pub description: Option<String>, pub(crate) description: Option<String>,
} }
pub(crate) async fn create(ctx: FullCtx, create: Create) -> color_eyre::Result<()> { pub(crate) async fn create(ctx: FullCtx, create: Create) -> color_eyre::Result<()> {
let dc = ctx.deck_client(); let dc = ctx.deck_client();

143
src/nextcloud/client.rs Normal file
View file

@ -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>) -> String {
f!(
"https://{}/index.php/apps/deck/api/v1.0/{}",
self.hostname,
path.into()
)
}
async fn request<T: for<'a> serde::Deserialize<'a>>(
&self,
url: impl Into<String>,
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")
.send()
.await,
&self.ctx.prt,
)
.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(crate) async fn get_boards(&self) -> APIResult<Vec<Board>> {
self.request("boards", |net, url| net.get(url)).await
}
pub(crate) async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult<Board> {
self.request(f!("boards/{board_id}"), |net, url| net.get(url))
.await
}
// pub(crate) async fn create_board(&self, title: &str, color: &str) -> APIResult<Board> {
// 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<Vec<Stack>> {
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<Stack> {
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<Card> {
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<Card> {
self.request(
f!("boards/{board_id}/stacks/{stack_id}/cards/{card_id}"),
|net, url| net.get(url),
)
.await
}
}

View file

@ -1,19 +1,30 @@
//
use bytes::Bytes; use bytes::Bytes;
//
use clap::Parser;
use kxio::net::{Net, ReqBuilder}; use kxio::net::{Net, ReqBuilder};
use crate::{api_result::APIResult, f, FullCtx}; use crate::{
api_result::APIResult,
use card::Create; execute::Execute,
use model::{ f,
nextcloud::{
board::NextcloudBoardCommand,
card::Create,
card::NextcloudCardCommand,
model::{
Board, Card, NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudStackId, Board, Card, NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudStackId,
NextcloudUsername, Stack, NextcloudUsername, Stack,
},
stack::NextcloudStackCommand,
},
FullCtx,
}; };
pub mod board; mod board;
pub mod card; mod card;
pub mod model; pub(crate) mod client;
pub mod stack; pub(crate) mod model;
mod stack;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -151,3 +162,30 @@ impl<'ctx> DeckClient<'ctx> {
.await .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,
}

View file

@ -113,64 +113,64 @@ newtype!(
); );
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct NextcloudBoardOwner { pub(crate) struct NextcloudBoardOwner {
#[serde(rename = "primaryKey")] #[serde(rename = "primaryKey")]
pub primary_key: String, pub(crate) primary_key: String,
pub uid: String, pub(crate) uid: String,
#[serde(rename = "displayname")] #[serde(rename = "displayname")]
pub display_name: String, pub(crate) display_name: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Board { pub(crate) struct Board {
pub id: NextcloudBoardId, pub(crate) id: NextcloudBoardId,
pub title: NextcloudBoardTitle, pub(crate) title: NextcloudBoardTitle,
pub owner: NextcloudBoardOwner, pub(crate) owner: NextcloudBoardOwner,
pub color: NextcloudBoardColour, pub(crate) color: NextcloudBoardColour,
pub archived: bool, pub(crate) archived: bool,
pub labels: Vec<Label>, pub(crate) labels: Vec<Label>,
pub acl: Vec<Acl>, pub(crate) acl: Vec<Acl>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Stack { pub(crate) struct Stack {
pub id: NextcloudStackId, pub(crate) id: NextcloudStackId,
pub title: NextcloudStackTitle, pub(crate) title: NextcloudStackTitle,
pub order: NextcloudOrder, pub(crate) order: NextcloudOrder,
#[serde(rename = "boardId")] #[serde(rename = "boardId")]
pub board_id: NextcloudBoardId, pub(crate) board_id: NextcloudBoardId,
#[serde(rename = "ETag")] #[serde(rename = "ETag")]
pub etag: NextcloudETag, pub(crate) etag: NextcloudETag,
#[serde(default)] #[serde(default)]
pub cards: Vec<Card>, pub(crate) cards: Vec<Card>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Card { pub(crate) struct Card {
pub id: NextcloudCardId, pub(crate) id: NextcloudCardId,
pub title: NextcloudCardTitle, pub(crate) title: NextcloudCardTitle,
pub description: Option<String>, pub(crate) description: Option<String>,
#[serde(rename = "stackId")] #[serde(rename = "stackId")]
pub stack_id: NextcloudStackId, pub(crate) stack_id: NextcloudStackId,
pub order: NextcloudOrder, pub(crate) order: NextcloudOrder,
pub archived: bool, pub(crate) archived: bool,
#[serde(default)] #[serde(default)]
pub due_date: Option<String>, pub(crate) due_date: Option<String>,
#[serde(default)] #[serde(default)]
pub labels: Option<Vec<Label>>, pub(crate) labels: Option<Vec<Label>>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Label { pub(crate) struct Label {
pub id: NextcloudLabelId, pub(crate) id: NextcloudLabelId,
pub title: String, pub(crate) title: String,
pub color: String, pub(crate) color: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Acl { pub(crate) struct Acl {
pub participant: String, pub(crate) participant: String,
pub permission_edit: bool, pub(crate) permission_edit: bool,
pub permission_share: bool, pub(crate) permission_share: bool,
pub permission_manage: bool, pub(crate) permission_manage: bool,
} }

View file

@ -1,7 +1,26 @@
// //
use clap::Parser;
use crate::execute::Execute;
use crate::{p, FullCtx}; use crate::{p, FullCtx};
pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> { #[derive(Parser, Debug)]
pub(crate) enum NextcloudStackCommand {
List {
#[clap(long, action = clap::ArgAction::SetTrue)]
dump: bool,
},
}
impl Execute for NextcloudStackCommand {
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 let api_result = ctx
.deck_client() .deck_client()
.get_stacks(ctx.cfg.nextcloud.board_id) .get_stacks(ctx.cfg.nextcloud.board_id)

View file

@ -1,26 +1,19 @@
// //
use kxio::{ use kxio::net::StatusCode;
fs::TempFileSystem,
net::{MockNet, StatusCode},
print::Printer,
};
use crate::{ use crate::{
config::{NextcloudConfig, TrelloConfig}, nextcloud::model::{
nextcloud::{
model::{
Board, Card, NextcloudBoardColour, NextcloudBoardId, NextcloudBoardOwner, Board, Card, NextcloudBoardColour, NextcloudBoardId, NextcloudBoardOwner,
NextcloudBoardTitle, NextcloudCardId, NextcloudCardTitle, NextcloudETag, NextcloudBoardTitle, NextcloudCardId, NextcloudCardTitle, NextcloudETag, NextcloudHostname,
NextcloudHostname, NextcloudOrder, NextcloudPassword, NextcloudStackId, NextcloudOrder, NextcloudPassword, NextcloudStackId, NextcloudStackTitle,
NextcloudStackTitle, NextcloudUsername, Stack, NextcloudUsername, Stack,
}, },
DeckClient, s,
},
s, AppConfig, FullCtx,
}; };
mod config { mod config {
use super::*; use super::*;
use crate::nextcloud::NextcloudConfig;
#[test] #[test]
fn config_hostname_returns_hostname() { fn config_hostname_returns_hostname() {
@ -118,12 +111,16 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-board-list.json")) .body(include_str!("../tests/responses/nextcloud-board-list.json"))
.expect("mock request"); .expect("mock request");
let fs = given::a_filesystem(); // let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs); let ctx = given::a_full_context(mock_net);
let deck_client = DeckClient::new(&ctx);
//when //when
let result = deck_client.get_boards().await.result.expect("get boards"); let result = ctx
.deck_client()
.get_boards()
.await
.result
.expect("get boards");
assert_eq!( assert_eq!(
result.first(), result.first(),
@ -162,12 +159,12 @@ mod commands {
)) ))
.expect("mock request"); .expect("mock request");
let fs = given::a_filesystem(); // let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs); let ctx = given::a_full_context(mock_net);
let deck_client = DeckClient::new(&ctx);
//when //when
let result = deck_client let result = ctx
.deck_client()
.get_stacks(ctx.cfg.nextcloud.board_id) .get_stacks(ctx.cfg.nextcloud.board_id)
.await .await
.result; .result;
@ -187,12 +184,12 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-stack-list.json")) .body(include_str!("../tests/responses/nextcloud-stack-list.json"))
.expect("mock request"); .expect("mock request");
let fs = given::a_filesystem(); // let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs); let ctx = given::a_full_context(mock_net);
let deck_client = DeckClient::new(&ctx);
//when //when
let result = deck_client let result = ctx
.deck_client()
.get_stacks(ctx.cfg.nextcloud.board_id) .get_stacks(ctx.cfg.nextcloud.board_id)
.await .await
.result .result
@ -305,12 +302,12 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-card-list.json")) .body(include_str!("../tests/responses/nextcloud-card-list.json"))
.expect("mock request"); .expect("mock request");
let fs = given::a_filesystem(); // let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs); let ctx = given::a_full_context(mock_net);
let deck_client = DeckClient::new(&ctx);
//when //when
let result = deck_client let result = ctx
.deck_client()
.get_stack(ctx.cfg.nextcloud.board_id, 1.into()) .get_stack(ctx.cfg.nextcloud.board_id, 1.into())
.await .await
.result .result
@ -351,12 +348,12 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-card-get.json")) .body(include_str!("../tests/responses/nextcloud-card-get.json"))
.expect("mock request"); .expect("mock request");
let fs = given::a_filesystem(); // let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs); let ctx = given::a_full_context(mock_net);
let deck_client = DeckClient::new(&ctx);
//when //when
let result = deck_client let result = ctx
.deck_client()
.get_card(ctx.cfg.nextcloud.board_id, 1.into(), 321.into()) .get_card(ctx.cfg.nextcloud.board_id, 1.into(), 321.into())
.await .await
.result .result
@ -405,12 +402,12 @@ mod commands {
)) ))
.expect("mock request"); .expect("mock request");
let fs = given::a_filesystem(); // let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs); let ctx = given::a_full_context(mock_net);
let deck_client = DeckClient::new(&ctx);
//when //when
let result = deck_client let result = ctx
.deck_client()
.create_card(&Create { .create_card(&Create {
dump: false, dump: false,
stack_id: 1, stack_id: 1,
@ -439,10 +436,16 @@ mod commands {
} }
mod given { mod given {
use kxio::{net::MockNet, print::Printer};
use super::*; use crate::nextcloud::model::{
NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername,
};
use crate::nextcloud::NextcloudConfig;
use crate::trello::TrelloConfig;
use crate::{s, AppConfig, FullCtx};
pub fn a_nextcloud_config() -> NextcloudConfig { pub(crate) fn a_nextcloud_config() -> NextcloudConfig {
let hostname = NextcloudHostname::new("host-name"); let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username"); let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password"); let password = NextcloudPassword::new("password");
@ -455,13 +458,13 @@ mod given {
} }
} }
pub fn a_printer() -> Printer { pub(crate) fn a_printer() -> Printer {
kxio::print::test() kxio::print::test()
} }
pub(crate) fn a_filesystem() -> TempFileSystem { // pub(crate) fn a_filesystem() -> TempFileSystem {
kxio::fs::temp().expect("temp fs") // kxio::fs::temp().expect("temp fs")
} // }
pub(crate) fn a_trello_config() -> TrelloConfig { pub(crate) fn a_trello_config() -> TrelloConfig {
TrelloConfig { TrelloConfig {
@ -471,14 +474,17 @@ mod given {
} }
} }
pub(crate) fn a_full_context(mock_net: MockNet, fs: TempFileSystem) -> FullCtx { pub(crate) fn a_full_context(
mock_net: MockNet,
// fs: TempFileSystem
) -> FullCtx {
FullCtx { FullCtx {
fs: fs.as_real(), // fs: fs.as_real(),
net: mock_net.into(), net: mock_net.into(),
prt: given::a_printer(), prt: a_printer(),
cfg: AppConfig { cfg: AppConfig {
trello: given::a_trello_config(), trello: a_trello_config(),
nextcloud: given::a_nextcloud_config(), nextcloud: a_nextcloud_config(),
}, },
} }
} }

View file

@ -5,14 +5,13 @@ use std::collections::HashMap;
use assert2::let_assert; use assert2::let_assert;
use crate::{config::AppConfig, f, init::run, Ctx, NAME}; use crate::{
config::AppConfig, f, init::run, nextcloud::NextcloudConfig, s, trello::TrelloConfig, Ctx, NAME,
};
mod config { mod config {
use super::*; use super::*;
use crate::config::{NextcloudConfig, TrelloConfig};
use crate::s;
#[test] #[test]
fn load_config() { fn load_config() {
//given //given
@ -124,18 +123,18 @@ mod given {
print::Printer, print::Printer,
}; };
pub fn a_context(fs: FileSystem, net: Net, prt: Printer) -> Ctx { pub(crate) fn a_context(fs: FileSystem, net: Net, prt: Printer) -> Ctx {
Ctx { fs, net, prt } Ctx { fs, net, prt }
} }
pub fn a_filesystem() -> TempFileSystem { pub(crate) fn a_filesystem() -> TempFileSystem {
kxio::fs::temp().expect("temp fs") kxio::fs::temp().expect("temp fs")
} }
pub fn a_network() -> MockNet { pub(crate) fn a_network() -> MockNet {
kxio::net::mock() kxio::net::mock()
} }
pub fn a_printer() -> Printer { pub(crate) fn a_printer() -> Printer {
kxio::print::test() kxio::print::test()
} }
} }

View file

@ -1,11 +1,7 @@
// //
// use color_eyre::Result;
// use kxio::net::Net;
use crate::trello::types::{board::TrelloBoard, TrelloBoardName}; use crate::trello::types::{board::TrelloBoard, TrelloBoardName};
// pub async fn get_board( // pub(crate) async fn get_board(
// auth: &TrelloAuth, // auth: &TrelloAuth,
// board_id: &TrelloBoardId, // board_id: &TrelloBoardId,
// net: &Net, // net: &Net,
@ -23,7 +19,7 @@ use crate::trello::types::{board::TrelloBoard, TrelloBoardName};
// Ok(board) // Ok(board)
// } // }
pub trait TrelloBoards { pub(crate) trait TrelloBoards {
fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard>; fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard>;
} }
impl TrelloBoards for Vec<TrelloBoard> { impl TrelloBoards for Vec<TrelloBoard> {

View file

@ -2,8 +2,3 @@ mod create;
mod delete; mod delete;
mod get; mod get;
mod update; mod update;
pub use create::create_card;
pub use delete::delete_card;
pub use get::get_card;
pub use update::{update_card, TrelloCardUpdate};

View file

@ -30,7 +30,7 @@ use crate::{
/// 200 OK Success /// 200 OK Success
/// ///
/// application/json /// application/json
pub async fn get_lists_cards( pub(crate) async fn get_lists_cards(
auth: &TrelloAuth, auth: &TrelloAuth,
list: &TrelloList, list: &TrelloList,
net: &Net, net: &Net,

View file

@ -1,11 +1,13 @@
// //
use kxio::{net::Net, print::Printer}; use kxio::{net::Net, print::Printer};
use crate::api_result::APIResult; use crate::trello::TrelloConfig;
use crate::config::TrelloConfig; use crate::{
use crate::trello::{ api_result::APIResult,
trello::{
types::{auth::TrelloAuth, board::TrelloBoard}, types::{auth::TrelloAuth, board::TrelloBoard},
url, url,
},
}; };
/// Get lists from named board that Member belongs to /// Get lists from named board that Member belongs to
@ -37,12 +39,15 @@ use crate::trello::{
/// curl --request GET \ /// curl --request GET \
/// --url "https://api.trello.com/1/members/$TRELLO_USERNAME/boards?key=$TRELLO_KEY&token=$TRELLO_SECRET&lists=open" \ /// --url "https://api.trello.com/1/members/$TRELLO_USERNAME/boards?key=$TRELLO_KEY&token=$TRELLO_SECRET&lists=open" \
/// --header 'Accept: application/json' /// --header 'Accept: application/json'
pub async fn get_boards_that_member_belongs_to( pub(crate) async fn get_boards_that_member_belongs_to(
cfg: &TrelloConfig, cfg: &TrelloConfig,
net: &Net, net: &Net,
prt: &Printer, prt: &Printer,
) -> APIResult<Vec<TrelloBoard>> { ) -> APIResult<Vec<TrelloBoard>> {
let auth = TrelloAuth::new(&cfg.api_key, &cfg.api_secret); let auth = TrelloAuth {
api_key: &cfg.api_key,
api_secret: &cfg.api_secret,
};
APIResult::new( APIResult::new(
net.get(url("/members/me/boards?lists=open")) net.get(url("/members/me/boards?lists=open"))
.headers(auth.into()) .headers(auth.into())

View file

@ -1,9 +1,9 @@
// //
pub mod boards; pub(crate) mod boards;
// pub mod cards; // pub(crate) mod cards;
// pub mod lists; // pub(crate) mod lists;
pub mod members; pub(crate) mod members;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View file

@ -5,7 +5,6 @@ use kxio::net::StatusCode;
use serde_json::json; use serde_json::json;
use crate::{ use crate::{
config::TrelloConfig,
s, s,
trello::{api::members::get_boards_that_member_belongs_to, types::board::TrelloBoard}, trello::{api::members::get_boards_that_member_belongs_to, types::board::TrelloBoard},
}; };
@ -15,8 +14,8 @@ mod given;
type TestResult = color_eyre::Result<()>; type TestResult = color_eyre::Result<()>;
mod members { mod members {
use super::*; use super::*;
use crate::trello::TrelloConfig;
#[tokio::test] #[tokio::test]
async fn get_member_boards() -> TestResult { async fn get_member_boards() -> TestResult {

View file

@ -1,18 +1,35 @@
// //
// use crate::{p, FullCtx}; use clap::Parser;
//
// pub(crate) async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> { use crate::execute::Execute;
// let api_result = use crate::{p, FullCtx};
// super::api::members::get_boards_that_member_belongs_to(&ctx.cfg.trello, &ctx.net, &ctx.prt)
// .await; #[derive(Parser, Debug)]
// if dump { pub(crate) enum TrelloBoardCommand {
// p!(ctx.prt, "{}", api_result.text); List {
// } else { #[clap(long, action = clap::ArgAction::SetTrue)]
// let mut boards = api_result.result?; dump: bool,
// boards.sort_by(|a, b| a.name.cmp(&b.name)); },
// boards.into_iter().for_each(|board| { }
// p!(ctx.prt, "{}:{}", board.id, board.name);
// }); impl Execute for TrelloBoardCommand {
// } async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> {
// Ok(()) 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.trello_client().boards(&ctx.cfg.trello).await;
if dump {
p!(ctx.prt, "{}", api_result.text);
} else {
let mut boards = api_result.result?;
boards.sort_by(|a, b| a.name.cmp(&b.name));
boards.into_iter().for_each(|board| {
p!(ctx.prt, "{}:{}", board.id, board.name);
});
}
Ok(())
}

22
src/trello/client.rs Normal file
View file

@ -0,0 +1,22 @@
//
use crate::api_result::APIResult;
use crate::trello::types::board::TrelloBoard;
use crate::trello::TrelloConfig;
use crate::FullCtx;
pub(crate) struct TrelloClient<'ctx> {
ctx: &'ctx FullCtx,
}
impl<'ctx> TrelloClient<'ctx> {
pub(crate) async fn boards(&self, cfg: &TrelloConfig) -> APIResult<Vec<TrelloBoard>> {
super::api::members::get_boards_that_member_belongs_to(cfg, &self.ctx.net, &self.ctx.prt)
.await
}
}
impl TrelloClient<'_> {
pub(crate) fn new(ctx: &FullCtx) -> TrelloClient {
TrelloClient { ctx }
}
}

View file

@ -1,16 +1,42 @@
// //
// pub mod api; pub(crate) mod api;
pub mod api; pub(crate) mod boards;
pub mod boards; pub(crate) mod client;
pub mod types; pub(crate) mod types;
// #[cfg(test)] // #[cfg(test)]
// mod tests; // mod tests;
use crate::f; use crate::execute::Execute;
use crate::trello::boards::TrelloBoardCommand;
use crate::trello::types::auth::{TrelloApiKey, TrelloApiSecret};
use crate::trello::types::TrelloBoardName;
use crate::{f, FullCtx};
use clap::Parser;
pub fn url(path: impl Into<String>) -> String { pub(crate) fn url(path: impl Into<String>) -> String {
let path = path.into(); let path = path.into();
assert!(path.starts_with("/")); assert!(path.starts_with("/"));
f!("https://api.trello.com/1{path}") f!("https://api.trello.com/1{path}")
} }
#[derive(Parser, Debug)]
pub(crate) enum TrelloCommand {
#[clap(subcommand)]
Board(TrelloBoardCommand),
}
impl Execute for TrelloCommand {
async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> {
match self {
Self::Board(cmd) => cmd.execute(ctx).await,
}
}
}
#[derive(Clone, Debug, derive_more::From, PartialEq, Eq, serde::Deserialize)]
pub struct TrelloConfig {
pub(crate) api_key: TrelloApiKey,
pub(crate) api_secret: TrelloApiSecret,
pub(crate) board_name: TrelloBoardName,
}

View file

@ -1,12 +1,14 @@
// //
use std::collections::HashMap; use std::collections::HashMap;
use crate::{s, trello::types::auth::TrelloAuth}; use crate::{
use crate::trello::{ s,
trello::types::auth::TrelloAuth,
trello::{
api::boards::TrelloBoards as _, api::boards::TrelloBoards as _,
types::{ types::{
TrelloBoard, TrelloBoardId, TrelloBoardName, TrelloList, TrelloListId, TrelloBoard, TrelloBoardId, TrelloBoardName, TrelloList, TrelloListId, TrelloListName,
TrelloListName, },
}, },
}; };
@ -58,6 +60,6 @@ fn trello_auth_into_hashmap() {
HashMap::from([( HashMap::from([(
s!("Authorization"), s!("Authorization"),
s!("OAuth oauth_consumer_key=\"key\", oauth_token=\"token\"") s!("OAuth oauth_consumer_key=\"key\", oauth_token=\"token\"")
), ]) ),])
); );
} }

View file

@ -18,29 +18,13 @@ pub struct TrelloAuth<'cfg> {
pub(crate) api_key: &'cfg TrelloApiKey, pub(crate) api_key: &'cfg TrelloApiKey,
pub(crate) api_secret: &'cfg TrelloApiSecret, pub(crate) api_secret: &'cfg TrelloApiSecret,
} }
impl<'cfg> TrelloAuth<'cfg> {
pub const fn new(api_key: &'cfg TrelloApiKey, api_secret: &'cfg TrelloApiSecret) -> Self {
Self {
api_key,
api_secret,
}
}
pub const fn api_key(&self) -> &TrelloApiKey {
self.api_key
}
pub const fn api_token(&self) -> &TrelloApiSecret {
self.api_secret
}
}
impl From<TrelloAuth<'_>> for HashMap<String, String> { impl From<TrelloAuth<'_>> for HashMap<String, String> {
fn from(value: TrelloAuth) -> Self { fn from(value: TrelloAuth) -> Self {
HashMap::from([( HashMap::from([(
"Authorization".into(), "Authorization".into(),
format!( format!(
r#"OAuth oauth_consumer_key="{}", oauth_token="{}""#, r#"OAuth oauth_consumer_key="{}", oauth_token="{}""#,
value.api_key(), value.api_key, value.api_secret
value.api_token()
), ),
)]) )])
} }

View file

@ -2,7 +2,7 @@
use crate::trello::{api::cards::TrelloCardUpdate, TrelloCardId, TrelloCardName, TrelloListId}; use crate::trello::{api::cards::TrelloCardUpdate, TrelloCardId, TrelloCardName, TrelloListId};
#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct TrelloCard { pub(crate) struct TrelloCard {
id: TrelloCardId, id: TrelloCardId,
name: TrelloCardName, name: TrelloCardName,
#[serde(rename = "idList")] #[serde(rename = "idList")]
@ -10,11 +10,11 @@ pub struct TrelloCard {
} }
impl TrelloCard { impl TrelloCard {
#[cfg(test)] #[cfg(test)]
pub const fn new(id: TrelloCardId, name: TrelloCardName, id_list: TrelloListId) -> Self { pub(crate) const fn new(id: TrelloCardId, name: TrelloCardName, id_list: TrelloListId) -> Self {
Self { id, name, id_list } Self { id, name, id_list }
} }
pub const fn list_id(&self) -> &TrelloListId { pub(crate) const fn list_id(&self) -> &TrelloListId {
&self.id_list &self.id_list
} }
} }

View file

@ -1,7 +1,7 @@
pub(crate) mod auth; pub(crate) mod auth;
pub(crate) mod board; pub(crate) mod board;
// mod card; // mod card;
mod list; pub(crate) mod list;
// mod new_card; // mod new_card;
use derive_more::derive::Display; use derive_more::derive::Display;

View file

@ -1,20 +1,20 @@
use super::{TrelloCardName, TrelloListId}; use super::{TrelloCardName, TrelloListId};
pub struct NewTrelloCard { pub(crate) struct NewTrelloCard {
name: TrelloCardName, name: TrelloCardName,
list_id: TrelloListId, list_id: TrelloListId,
} }
impl NewTrelloCard { impl NewTrelloCard {
pub fn new(name: impl Into<TrelloCardName>, list_id: impl Into<TrelloListId>) -> Self { pub(crate) fn new(name: impl Into<TrelloCardName>, list_id: impl Into<TrelloListId>) -> Self {
Self { Self {
name: name.into(), name: name.into(),
list_id: list_id.into(), list_id: list_id.into(),
} }
} }
pub const fn name(&self) -> &TrelloCardName { pub(crate) const fn name(&self) -> &TrelloCardName {
&self.name &self.name
} }
pub const fn list_id(&self) -> &TrelloListId { pub(crate) const fn list_id(&self) -> &TrelloListId {
&self.list_id &self.list_id
} }
} }