feat(trello): add command 'trello attachement save'
This commit is contained in:
parent
0e8b2e8720
commit
d6c299e249
9 changed files with 178 additions and 43 deletions
|
@ -73,6 +73,7 @@ As part of building the import server, the following commands exercise each oper
|
||||||
- [x] trello stack get - includes list of cards
|
- [x] trello stack get - includes list of cards
|
||||||
- [x] trello card get - includes list of attachments
|
- [x] trello card get - includes list of attachments
|
||||||
- [x] trello attachment get - includes download url
|
- [x] trello attachment get - includes download url
|
||||||
|
- [x] trello attachment save - saves to disk
|
||||||
- [ ] nextcloud deck get (was board list)
|
- [ ] nextcloud deck get (was board list)
|
||||||
- [ ] nextcloud board get (was stack list)
|
- [ ] nextcloud board get (was stack list)
|
||||||
- [ ] nextcloud stack get (was card list)
|
- [ ] nextcloud stack get (was card list)
|
||||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -67,10 +67,10 @@ pub struct Ctx {
|
||||||
pub net: Net,
|
pub net: Net,
|
||||||
pub prt: Printer,
|
pub prt: Printer,
|
||||||
}
|
}
|
||||||
impl Default for Ctx {
|
impl Ctx {
|
||||||
fn default() -> Self {
|
pub fn new(base: impl Into<PathBuf>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
fs: kxio::fs::new(PathBuf::default()),
|
fs: kxio::fs::new(base),
|
||||||
net: kxio::net::new(),
|
net: kxio::net::new(),
|
||||||
prt: kxio::print::standard(),
|
prt: kxio::print::standard(),
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ impl Default for Ctx {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) 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,
|
||||||
|
@ -121,7 +121,7 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> {
|
||||||
commands
|
commands
|
||||||
.command
|
.command
|
||||||
.execute(FullCtx {
|
.execute(FullCtx {
|
||||||
// fs: ctx.fs,
|
fs: ctx.fs,
|
||||||
net: ctx.net,
|
net: ctx.net,
|
||||||
prt: ctx.prt,
|
prt: ctx.prt,
|
||||||
cfg,
|
cfg,
|
||||||
|
|
|
@ -6,5 +6,5 @@ use trello_to_deck::{run, Ctx};
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
#[cfg_attr(test, mutants::skip)]
|
#[cfg_attr(test, mutants::skip)]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
run(Ctx::default()).await
|
run(Ctx::new(std::env::current_dir()?)).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,8 +126,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
let prt = ctx.prt.clone();
|
let prt = ctx.prt.clone();
|
||||||
let prt = prt.as_test().unwrap();
|
let prt = prt.as_test().unwrap();
|
||||||
|
|
||||||
|
@ -159,8 +159,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
let prt = ctx.prt.clone();
|
let prt = ctx.prt.clone();
|
||||||
let prt = prt.as_test().unwrap();
|
let prt = prt.as_test().unwrap();
|
||||||
|
|
||||||
|
@ -203,8 +203,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
@ -228,8 +228,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
@ -340,8 +340,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
@ -386,8 +386,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
@ -440,8 +440,8 @@ 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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
@ -492,8 +492,8 @@ mod commands {
|
||||||
.mock()
|
.mock()
|
||||||
.expect("mock request");
|
.expect("mock request");
|
||||||
|
|
||||||
// let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let ctx = given::a_full_context(mock_net);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
|
|
@ -124,12 +124,9 @@ pub(crate) mod given {
|
||||||
Ctx { fs, net, prt }
|
Ctx { fs, net, prt }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn a_full_context(
|
pub(crate) fn a_full_context(mock_net: MockNet, fs: TempFileSystem) -> FullCtx {
|
||||||
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: a_printer(),
|
prt: a_printer(),
|
||||||
cfg: AppConfig {
|
cfg: AppConfig {
|
||||||
|
|
BIN
src/tests/responses/trello-attachment-save.png
Normal file
BIN
src/tests/responses/trello-attachment-save.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 180 KiB |
|
@ -1,3 +1,5 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
//
|
//
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
@ -15,6 +17,11 @@ pub(crate) enum TrelloAttachmentCommand {
|
||||||
card_id: String,
|
card_id: String,
|
||||||
attachment_id: String,
|
attachment_id: String,
|
||||||
},
|
},
|
||||||
|
Save {
|
||||||
|
card_id: String,
|
||||||
|
attachment_id: String,
|
||||||
|
file_name: Option<PathBuf>, // will use file name from attachment if not provided.
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Execute for TrelloAttachmentCommand {
|
impl Execute for TrelloAttachmentCommand {
|
||||||
|
@ -25,12 +32,11 @@ impl Execute for TrelloAttachmentCommand {
|
||||||
card_id,
|
card_id,
|
||||||
attachment_id,
|
attachment_id,
|
||||||
} => {
|
} => {
|
||||||
|
let trello_card_id = TrelloCardId::new(card_id);
|
||||||
|
let trello_attachment_id = TrelloAttachmentId::new(attachment_id);
|
||||||
let api_result = ctx
|
let api_result = ctx
|
||||||
.trello_client()
|
.trello_client()
|
||||||
.card_attachment(
|
.card_attachment(&trello_card_id, &trello_attachment_id)
|
||||||
&TrelloCardId::new(card_id),
|
|
||||||
&TrelloAttachmentId::new(attachment_id),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
if dump {
|
if dump {
|
||||||
p!(ctx.prt, "{}", api_result.text);
|
p!(ctx.prt, "{}", api_result.text);
|
||||||
|
@ -40,6 +46,20 @@ impl Execute for TrelloAttachmentCommand {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Self::Save {
|
||||||
|
card_id,
|
||||||
|
attachment_id,
|
||||||
|
file_name,
|
||||||
|
} => {
|
||||||
|
let trello_card_id = TrelloCardId::new(card_id);
|
||||||
|
let trello_attachment_id = TrelloAttachmentId::new(attachment_id);
|
||||||
|
let file_name = ctx
|
||||||
|
.trello_client()
|
||||||
|
.save_attachment(&trello_card_id, &trello_attachment_id, file_name)
|
||||||
|
.await?;
|
||||||
|
p!(ctx.prt, "Wrote: {}", file_name.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
//
|
//
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use color_eyre::eyre::Context;
|
||||||
use kxio::net::{Net, ReqBuilder};
|
use kxio::net::{Net, ReqBuilder};
|
||||||
|
|
||||||
use crate::trello::model::{TrelloAttachment, TrelloAttachmentId};
|
use crate::trello::model::{TrelloAttachment, TrelloAttachmentId};
|
||||||
|
@ -26,6 +28,14 @@ impl<'ctx> TrelloClient<'ctx> {
|
||||||
f!("https://api.trello.com/1{path}")
|
f!("https://api.trello.com/1{path}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auth_headers(&self) -> HashMap<String, String> {
|
||||||
|
let api_key = &self.ctx.cfg.trello.api_key;
|
||||||
|
let api_secret = &self.ctx.cfg.trello.api_secret;
|
||||||
|
HashMap::from([(
|
||||||
|
s!("Authorization"),
|
||||||
|
f!(r#"OAuth oauth_consumer_key="{api_key}", oauth_token="{api_secret}""#,),
|
||||||
|
)])
|
||||||
|
}
|
||||||
fn common_headers(&self) -> HashMap<String, String> {
|
fn common_headers(&self) -> HashMap<String, String> {
|
||||||
let api_key = &self.ctx.cfg.trello.api_key;
|
let api_key = &self.ctx.cfg.trello.api_key;
|
||||||
let api_secret = &self.ctx.cfg.trello.api_secret;
|
let api_secret = &self.ctx.cfg.trello.api_secret;
|
||||||
|
@ -53,6 +63,34 @@ impl<'ctx> TrelloClient<'ctx> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn save_attachment(
|
||||||
|
&self,
|
||||||
|
card_id: &TrelloCardId,
|
||||||
|
attachment_id: &TrelloAttachmentId,
|
||||||
|
file_name: Option<std::path::PathBuf>,
|
||||||
|
) -> color_eyre::Result<PathBuf> {
|
||||||
|
let attachment = self.card_attachment(card_id, attachment_id).await.result?;
|
||||||
|
let url = attachment.url;
|
||||||
|
let file_name = file_name.unwrap_or_else(|| attachment.file_name.into());
|
||||||
|
crate::e!(self.ctx.prt, "file_name: {}", file_name.display());
|
||||||
|
crate::e!(self.ctx.prt, "base: {}", self.ctx.fs.base().display());
|
||||||
|
let file_name = self.ctx.fs.base().join(file_name);
|
||||||
|
crate::e!(self.ctx.prt, "file_name: {}", file_name.display());
|
||||||
|
let resp = self
|
||||||
|
.ctx
|
||||||
|
.net
|
||||||
|
.get(url)
|
||||||
|
.headers(self.auth_headers())
|
||||||
|
.header("accept", "application/octet")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.bytes()
|
||||||
|
.await?;
|
||||||
|
let file = self.ctx.fs.file(&file_name);
|
||||||
|
file.write(resp).context("writing to disk")?;
|
||||||
|
Ok(file_name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ctx> TrelloClient<'ctx> {
|
impl<'ctx> TrelloClient<'ctx> {
|
||||||
|
|
|
@ -73,8 +73,8 @@ mod commands {
|
||||||
.body(include_str!("../tests/responses/trello-board-get.json"))
|
.body(include_str!("../tests/responses/trello-board-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);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let result = ctx
|
let result = ctx
|
||||||
|
@ -121,8 +121,8 @@ mod commands {
|
||||||
|
|
||||||
prep_mock_get(&mock_net);
|
prep_mock_get(&mock_net);
|
||||||
|
|
||||||
// let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let ctx = given::a_full_context(mock_net);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
let prt = ctx.prt.clone();
|
let prt = ctx.prt.clone();
|
||||||
let prt = prt.as_test().unwrap();
|
let prt = prt.as_test().unwrap();
|
||||||
let command = Command::Trello(TrelloCommand::Card(TrelloCardCommand::Get {
|
let command = Command::Trello(TrelloCommand::Card(TrelloCardCommand::Get {
|
||||||
|
@ -145,8 +145,8 @@ mod commands {
|
||||||
|
|
||||||
prep_mock_get(&mock_net);
|
prep_mock_get(&mock_net);
|
||||||
|
|
||||||
// let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let ctx = given::a_full_context(mock_net);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
let prt = ctx.prt.clone();
|
let prt = ctx.prt.clone();
|
||||||
let prt = prt.as_test().unwrap();
|
let prt = prt.as_test().unwrap();
|
||||||
let command = Command::Trello(TrelloCommand::Card(TrelloCardCommand::Get {
|
let command = Command::Trello(TrelloCommand::Card(TrelloCardCommand::Get {
|
||||||
|
@ -192,8 +192,8 @@ mod commands {
|
||||||
|
|
||||||
prep_mock_get(&mock_net);
|
prep_mock_get(&mock_net);
|
||||||
|
|
||||||
// let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let ctx = given::a_full_context(mock_net);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
let prt = ctx.prt.clone();
|
let prt = ctx.prt.clone();
|
||||||
let prt = prt.as_test().unwrap();
|
let prt = prt.as_test().unwrap();
|
||||||
let command =
|
let command =
|
||||||
|
@ -204,7 +204,7 @@ mod commands {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
command.execute(ctx).await;
|
command.execute(ctx).await.expect("execute");
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let output = prt.output();
|
let output = prt.output();
|
||||||
|
@ -218,8 +218,8 @@ mod commands {
|
||||||
|
|
||||||
prep_mock_get(&mock_net);
|
prep_mock_get(&mock_net);
|
||||||
|
|
||||||
// let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let ctx = given::a_full_context(mock_net);
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
let prt = ctx.prt.clone();
|
let prt = ctx.prt.clone();
|
||||||
let prt = prt.as_test().unwrap();
|
let prt = prt.as_test().unwrap();
|
||||||
let command =
|
let command =
|
||||||
|
@ -230,7 +230,7 @@ mod commands {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
command.execute(ctx).await;
|
command.execute(ctx).await.expect("execute");
|
||||||
|
|
||||||
//then
|
//then
|
||||||
let output = prt.output();
|
let output = prt.output();
|
||||||
|
@ -252,5 +252,84 @@ mod commands {
|
||||||
.expect("mock request");
|
.expect("mock request");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod save {
|
||||||
|
use super::*;
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn save_writes_file() {
|
||||||
|
//given
|
||||||
|
let mock_net = kxio::net::mock();
|
||||||
|
|
||||||
|
prep_mock_get(&mock_net);
|
||||||
|
|
||||||
|
let fs = given::a_filesystem();
|
||||||
|
let test_fs = fs.clone();
|
||||||
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
let command =
|
||||||
|
Command::Trello(TrelloCommand::Attachment(TrelloAttachmentCommand::Save {
|
||||||
|
card_id: s!("65ad94865aed24f70ecdcebb"),
|
||||||
|
attachment_id: s!("65ad94875aed24f70ecdd037"),
|
||||||
|
file_name: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//when
|
||||||
|
command.execute(ctx).await.expect("execute");
|
||||||
|
|
||||||
|
//then
|
||||||
|
let file = test_fs.base().join("Backlog.png");
|
||||||
|
assert!(file.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn save_log_output() {
|
||||||
|
//given
|
||||||
|
let mock_net = kxio::net::mock();
|
||||||
|
|
||||||
|
prep_mock_get(&mock_net);
|
||||||
|
|
||||||
|
let fs = given::a_filesystem();
|
||||||
|
let test_fs = fs.clone();
|
||||||
|
let ctx = given::a_full_context(mock_net, fs);
|
||||||
|
let prt = ctx.prt.clone();
|
||||||
|
let prt = prt.as_test().unwrap();
|
||||||
|
let command =
|
||||||
|
Command::Trello(TrelloCommand::Attachment(TrelloAttachmentCommand::Save {
|
||||||
|
card_id: s!("65ad94865aed24f70ecdcebb"),
|
||||||
|
attachment_id: s!("65ad94875aed24f70ecdd037"),
|
||||||
|
file_name: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//when
|
||||||
|
command.execute(ctx).await.expect("execute");
|
||||||
|
|
||||||
|
//then
|
||||||
|
let file = test_fs.base().join("Backlog.png");
|
||||||
|
drop(test_fs);
|
||||||
|
let output = prt.output();
|
||||||
|
assert_eq!(output.trim(), crate::f!("Wrote: {}", file.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prep_mock_get(mock_net: &MockNet) {
|
||||||
|
mock_net
|
||||||
|
.on()
|
||||||
|
.get("https://api.trello.com/1/cards/65ad94865aed24f70ecdcebb/attachments/65ad94875aed24f70ecdd037")
|
||||||
|
.header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"")
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.respond(StatusCode::OK)
|
||||||
|
.body(include_str!("../tests/responses/trello-attachment-get.json"))
|
||||||
|
.expect("mock request 1");
|
||||||
|
|
||||||
|
mock_net
|
||||||
|
.on()
|
||||||
|
.get("https://trello.com/1/cards/65ad94865aed24f70ecdcebb/attachments/65ad94875aed24f70ecdd037/download/Backlog.png")
|
||||||
|
.header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"")
|
||||||
|
.respond(StatusCode::OK)
|
||||||
|
.body(Bytes::from(include_bytes!("../tests/responses/trello-attachment-save.png").as_slice()))
|
||||||
|
.expect("mock request 2");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue