feat: split into actors
Some checks failed
Test / build (map[name:nightly]) (push) Successful in 1m59s
Test / build (map[name:stable]) (push) Successful in 2m18s
Release Please / Release-plz (push) Failing after 13s

This commit is contained in:
Paul Campbell 2024-12-20 21:01:33 +00:00
parent 61323484a2
commit 0e123898db
15 changed files with 1070 additions and 275 deletions

View file

@ -15,6 +15,7 @@ derive_more = { version = "1.0", features = [
"from",
] }
inquire = "0.7"
kameo = "0.13"
# kxio = {path = "../kxio/"}
kxio = "4.0"
reqwest = { version = "0.12" , features = ["multipart", "stream"]}

View file

@ -1,56 +1,123 @@
//
use std::path::PathBuf;
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
use crate::{
nextcloud::{
client::DeckClient,
model::{Card, NextcloudBoardId, NextcloudStackId},
},
p,
trello::{
client::TrelloClient,
model::{
nextcloud::model::{NextcloudBoardId, NextcloudCardId, NextcloudStackId},
on_actor_start, p,
trello::model::{
attachment::{TrelloAttachment, TrelloAttachmentId},
card::TrelloShortCard,
},
card::TrelloCardId,
},
FullCtx,
};
pub(super) async fn import_attachment(
ctx: &FullCtx,
// pub(super) async fn import_attachment(
// ctx: &FullCtx,
// nextcloud_board_id: NextcloudBoardId,
// nextcloud_stack_id: NextcloudStackId,
// trello_card: &TrelloShortCard,
// nextcloud_card: &Card,
// trello_attachment: &TrelloAttachment,
// ) -> color_eyre::Result<()> {
// let trello_client: TrelloClient = ctx.trello_client();
//
// let deck_client: DeckClient = ctx.deck_client();
//
// p!(ctx.prt, ">> Adding attachment: {}", trello_attachment.name);
// // - - - download the attachment from trello
// let attachment_path = trello_client
// .save_attachment(
// &trello_card.id,
// &TrelloAttachmentId::new(&trello_attachment.id),
// Some(&PathBuf::from(&trello_attachment.id)),
// )
// .await?;
// let attachment_file = ctx.fs.file(&attachment_path);
// // - - - upload the attachment to nextcloud card
// let attachment = deck_client
// .add_attachment_to_card(
// nextcloud_board_id,
// nextcloud_stack_id,
// nextcloud_card.id,
// &attachment_file,
// )
// .await
// .result?;
// p!(
// ctx.prt,
// ">> Attachment added: {}:{}",
// attachment.id,
// attachment.attachment_type,
// // attachment.extended_data.mimetype
// );
// // delete local copy of attachment
// attachment_file.remove()?;
// Ok(())
// }
pub(crate) struct ImportAttachmentActor {
ctx: FullCtx,
nextcloud_board_id: NextcloudBoardId,
nextcloud_stack_id: NextcloudStackId,
trello_card: &TrelloShortCard,
nextcloud_card: &Card,
trello_attachment: &TrelloAttachment,
) -> color_eyre::Result<()> {
let trello_client: TrelloClient = ctx.trello_client();
let deck_client: DeckClient = ctx.deck_client();
p!(ctx.prt, ">> Adding attachment: {}", trello_attachment.name);
// - - - download the attachment from trello
let attachment_path = trello_client
.save_attachment(
&trello_card.id,
&TrelloAttachmentId::new(&trello_attachment.id),
Some(&PathBuf::from(&trello_attachment.id)),
)
.await?;
let attachment_file = ctx.fs.file(&attachment_path);
// - - - upload the attachment to nextcloud card
let attachment = deck_client
.add_attachment_to_card(
nextcloud_card_id: NextcloudCardId,
trello_card_id: TrelloCardId,
trello_attachment: TrelloAttachment,
}
impl ImportAttachmentActor {
pub fn new(
ctx: FullCtx,
nextcloud_board_id: NextcloudBoardId,
nextcloud_stack_id: NextcloudStackId,
nextcloud_card_id: NextcloudCardId,
trello_card_id: TrelloCardId,
trello_attachment: TrelloAttachment,
) -> Self {
Self {
ctx,
nextcloud_board_id,
nextcloud_stack_id,
nextcloud_card.id,
nextcloud_card_id,
trello_card_id,
trello_attachment,
}
}
}
impl Actor for ImportAttachmentActor {
type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
p!(
this.ctx.prt,
">> Adding attachment: {}",
this.trello_attachment.name
);
// - - - download the attachment from trello
let attachment_path = this
.ctx
.trello_client()
.save_attachment(
&this.trello_card_id,
&TrelloAttachmentId::new(&this.trello_attachment.id),
Some(&PathBuf::from(&this.trello_attachment.id)),
)
.await?;
let attachment_file = this.ctx.fs.file(&attachment_path);
// - - - upload the attachment to nextcloud card
let attachment = this
.ctx
.deck_client()
.add_attachment_to_card(
this.nextcloud_board_id,
this.nextcloud_stack_id,
this.nextcloud_card_id,
&attachment_file,
)
.await
.result?;
p!(
ctx.prt,
this.ctx.prt,
">> Attachment added: {}:{}",
attachment.id,
attachment.attachment_type,
@ -58,5 +125,7 @@ pub(super) async fn import_attachment(
);
// delete local copy of attachment
attachment_file.remove()?;
Ok(())
Ok(actor_ref.stop_gracefully().await?)
});
}

View file

@ -1,75 +1,194 @@
//
use std::collections::HashMap;
use kameo::{
actor::{ActorID, ActorRef},
error::ActorStopReason,
mailbox::unbounded::UnboundedMailbox,
Actor,
};
use crate::{
import::{attachment, label},
import::{attachment::ImportAttachmentActor, label::ImportLabelActor, labels::LabelsActor},
nextcloud::{
client::DeckClient,
model::{
Label, NextcloudBoardId, NextcloudCardDescription, NextcloudCardTitle,
NextcloudLabelTitle, NextcloudStackId,
model::{NextcloudBoardId, NextcloudCardDescription, NextcloudCardTitle, NextcloudStackId},
},
},
p,
on_actor_link_died, on_actor_start, p, spawn_in_thread,
trello::{client::TrelloClient, model::card::TrelloShortCard},
FullCtx,
};
pub(super) async fn import_card(
ctx: &FullCtx,
// pub(super) async fn import_card(
// ctx: &FullCtx,
// nextcloud_board_id: NextcloudBoardId,
// nextcloud_labels: &mut HashMap<NextcloudLabelTitle, Label>,
// nextcloud_stack_id: NextcloudStackId,
// trello_card: &TrelloShortCard,
// ) -> color_eyre::Result<()> {
// let trello_client: TrelloClient = ctx.trello_client();
//
// let deck_client: DeckClient = ctx.deck_client();
//
// p!(ctx.prt, "> Importing card: {}", trello_card.name);
// // - - create a nextcloud card
// let title = NextcloudCardTitle::from(&trello_card.name);
// let desc: Option<NextcloudCardDescription> = match trello_card.desc.len() {
// 0 => None,
// _ => Some(NextcloudCardDescription::from(&trello_card.desc)),
// };
// let nextcloud_card = deck_client
// .create_card(
// nextcloud_board_id,
// nextcloud_stack_id,
// &title,
// desc.as_ref(),
// )
// .await
// .result?;
// // - - for each label on the trello card
// for trello_label in trello_card.labels.iter() {
// labels::import_label(
// ctx,
// nextcloud_board_id,
// nextcloud_labels,
// nextcloud_stack_id,
// &nextcloud_card,
// trello_label,
// )
// .await?;
// }
// // - - for each attachment on the trello card
// let attachments = trello_client
// .card(&trello_card.id)
// .await
// .result?
// .attachments;
// for trello_attachment in attachments.iter() {
// attachment::import_attachment(
// ctx,
// nextcloud_board_id,
// nextcloud_stack_id,
// trello_card,
// &nextcloud_card,
// trello_attachment,
// )
// .await?;
// }
// Ok(())
// }
pub(crate) struct ImportCardActor {
ctx: FullCtx,
trello_card: TrelloShortCard,
nextcloud_board_id: NextcloudBoardId,
nextcloud_labels: &mut HashMap<NextcloudLabelTitle, Label>,
nextcloud_stack_id: NextcloudStackId,
trello_card: &TrelloShortCard,
) -> color_eyre::Result<()> {
let trello_client: TrelloClient = ctx.trello_client();
labels_actor_ref: ActorRef<LabelsActor>,
let deck_client: DeckClient = ctx.deck_client();
labels_children: HashMap<ActorID, ActorRef<ImportLabelActor>>,
attachments_children: HashMap<ActorID, ActorRef<ImportAttachmentActor>>,
}
impl ImportCardActor {
pub(crate) fn new(
ctx: FullCtx,
trello_card: TrelloShortCard,
nextcloud_board_id: NextcloudBoardId,
nextcloud_stack_id: NextcloudStackId,
labels_actor_ref: ActorRef<LabelsActor>,
) -> Self {
Self {
ctx,
trello_card,
nextcloud_board_id,
nextcloud_stack_id,
labels_actor_ref,
p!(ctx.prt, "> Importing card: {}", trello_card.name);
labels_children: Default::default(),
attachments_children: Default::default(),
}
}
}
impl Actor for ImportCardActor {
type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
let trello_client: TrelloClient = this.ctx.trello_client();
let deck_client: DeckClient = this.ctx.deck_client();
p!(this.ctx.prt, "> Importing card: {}", this.trello_card.name);
// - - create a nextcloud card
let title = NextcloudCardTitle::from(&trello_card.name);
let desc: Option<NextcloudCardDescription> = match trello_card.desc.len() {
let title = NextcloudCardTitle::from(&this.trello_card.name);
let desc: Option<NextcloudCardDescription> = match this.trello_card.desc.len() {
0 => None,
_ => Some(NextcloudCardDescription::from(&trello_card.desc)),
_ => Some(NextcloudCardDescription::from(&this.trello_card.desc)),
};
let nextcloud_card = deck_client
.create_card(
nextcloud_board_id,
nextcloud_stack_id,
this.nextcloud_board_id,
this.nextcloud_stack_id,
&title,
desc.as_ref(),
)
.await
.result?;
// - - for each label on the trello card
for trello_label in trello_card.labels.iter() {
label::import_label(
ctx,
nextcloud_board_id,
nextcloud_labels,
nextcloud_stack_id,
&nextcloud_card,
let mut labels = vec![];
std::mem::swap(&mut this.trello_card.labels, &mut labels);
for trello_label in labels.into_iter() {
let child = spawn_in_thread!(
actor_ref,
ImportLabelActor::new(
this.ctx.clone(),
this.nextcloud_board_id,
this.nextcloud_stack_id,
nextcloud_card.id,
trello_label,
this.labels_actor_ref.clone(),
)
.await?;
);
this.labels_children.insert(child.id(), child.clone());
}
// - - for each attachment on the trello card
let attachments = trello_client
.card(&trello_card.id)
.card(&this.trello_card.id)
.await
.result?
.attachments;
for trello_attachment in attachments.iter() {
attachment::import_attachment(
ctx,
nextcloud_board_id,
nextcloud_stack_id,
trello_card,
&nextcloud_card,
for trello_attachment in attachments.into_iter() {
let child = spawn_in_thread!(
actor_ref,
ImportAttachmentActor::new(
this.ctx.clone(),
this.nextcloud_board_id,
this.nextcloud_stack_id,
nextcloud_card.id,
this.trello_card.id.clone(),
trello_attachment,
)
.await?;
);
this.attachments_children.insert(child.id(), child.clone());
}
Ok(())
});
on_actor_link_died!(this, actor_ref, id, reason, {
match reason {
ActorStopReason::Normal => {
this.labels_children.remove(&id);
this.attachments_children.remove(&id);
}
_ => {
return Ok(Some(reason));
}
}
if this.labels_children.is_empty() && this.attachments_children.is_empty() {
return Ok(Some(ActorStopReason::Normal));
}
Ok(None)
});
}

View file

@ -1,67 +1,65 @@
//
use std::collections::HashMap;
use all_colors::get_color_hex;
use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, Actor};
use crate::{
nextcloud::{
client::DeckClient,
model::{
Card, Label, NextcloudBoardId, NextcloudLabelColour, NextcloudLabelTitle,
NextcloudStackId,
},
},
p,
ask,
import::{labels, labels::LabelsActor},
nextcloud::model::{NextcloudBoardId, NextcloudCardId, NextcloudStackId},
on_actor_start,
trello::model::label::TrelloLabel,
FullCtx,
};
pub(super) async fn import_label(
ctx: &FullCtx,
pub(crate) struct ImportLabelActor {
ctx: FullCtx,
nextcloud_board_id: NextcloudBoardId,
nextcloud_labels: &mut HashMap<NextcloudLabelTitle, Label>,
nextcloud_stack_id: NextcloudStackId,
nextcloud_card: &Card,
trello_label: &TrelloLabel,
) -> color_eyre::Result<()> {
let deck_client: DeckClient = ctx.deck_client();
p!(
ctx.prt,
">> Adding label: {} ({})",
trello_label.name,
trello_label.color
);
// - - - find the equivalent label in nextcloud
let nextcloud_label: &Label =
match nextcloud_labels.get(&NextcloudLabelTitle::new(trello_label.name.as_ref())) {
Some(label) => label,
None => {
p!(ctx.prt, ">> Label not found in nextcloud board, creating");
let label = deck_client
.create_label(
nextcloud_board_id,
&NextcloudLabelTitle::new(trello_label.name.as_ref()),
&NextcloudLabelColour::new(get_color_hex(trello_label.color.as_ref())),
)
.await
.result?;
nextcloud_labels.insert(label.title.clone(), label);
nextcloud_labels
.get(&NextcloudLabelTitle::new(trello_label.name.as_ref()))
.expect("label was just inserted")
nextcloud_card_id: NextcloudCardId,
trello_label: TrelloLabel,
labels_actor_ref: ActorRef<LabelsActor>,
}
};
// - - - add the label to the nextcloud card
deck_client
.add_label_to_card(
impl ImportLabelActor {
pub(crate) fn new(
ctx: FullCtx,
nextcloud_board_id: NextcloudBoardId,
nextcloud_stack_id: NextcloudStackId,
nextcloud_card_id: NextcloudCardId,
trello_label: TrelloLabel,
labels_actor_ref: ActorRef<LabelsActor>,
) -> Self {
Self {
ctx,
nextcloud_board_id,
nextcloud_stack_id,
nextcloud_card.id,
nextcloud_label.id,
nextcloud_card_id,
trello_label,
labels_actor_ref,
}
}
}
impl Actor for ImportLabelActor {
type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
let label_id = ask!(
this.labels_actor_ref,
labels::LookupNextcloudLabelId {
trello_name: this.trello_label.name.clone(),
trello_color: this.trello_label.color.clone()
}
)?;
// - - - add the label to the nextcloud card
this.ctx
.deck_client()
.add_label_to_card(
this.nextcloud_board_id,
this.nextcloud_stack_id,
this.nextcloud_card_id,
label_id,
)
.await
.result?;
Ok(())
Ok(actor_ref.stop_gracefully().await?)
});
}

142
src/import/labels.rs Normal file
View file

@ -0,0 +1,142 @@
//
use std::collections::HashMap;
use all_colors::get_color_hex;
use kameo::{
mailbox::unbounded::UnboundedMailbox,
message::{Context, Message},
Actor,
};
use crate::{
nextcloud::model::{
NextcloudBoardId, NextcloudLabelColour, NextcloudLabelId, NextcloudLabelTitle,
},
on_actor_start, p,
trello::model::label::{TrelloLabelColor, TrelloLabelName},
FullCtx,
};
// pub(super) async fn import_label(
// ctx: &FullCtx,
// nextcloud_board_id: NextcloudBoardId,
// nextcloud_labels: &mut HashMap<NextcloudLabelTitle, Label>,
// nextcloud_stack_id: NextcloudStackId,
// nextcloud_card: &Card,
// trello_label: &TrelloLabel,
// ) -> color_eyre::Result<()> {
// let deck_client: DeckClient = ctx.deck_client();
//
// p!(
// ctx.prt,
// ">> Adding label: {} ({})",
// trello_label.name,
// trello_label.color
// );
// // - - - find the equivalent label in nextcloud
// let nextcloud_label: &Label =
// match nextcloud_labels.get(&NextcloudLabelTitle::new(trello_label.name.as_ref())) {
// Some(label) => label,
// None => {
// p!(ctx.prt, ">> Label not found in nextcloud board, creating");
//
// let label = deck_client
// .create_label(
// nextcloud_board_id,
// &NextcloudLabelTitle::new(trello_label.name.as_ref()),
// &NextcloudLabelColour::new(get_color_hex(trello_label.color.as_ref())),
// )
// .await
// .result?;
// nextcloud_labels.insert(label.title.clone(), label);
// nextcloud_labels
// .get(&NextcloudLabelTitle::new(trello_label.name.as_ref()))
// .expect("label was just inserted")
// }
// };
// // - - - add the label to the nextcloud card
// deck_client
// .add_label_to_card(
// nextcloud_board_id,
// nextcloud_stack_id,
// nextcloud_card.id,
// nextcloud_label.id,
// )
// .await
// .result?;
// Ok(())
// }
pub(super) struct LabelsActor {
ctx: FullCtx,
nextcloud_board_id: NextcloudBoardId,
lookup: HashMap<NextcloudLabelTitle, NextcloudLabelId>,
}
impl LabelsActor {
pub(super) fn new(ctx: FullCtx, nextcloud_board_id: NextcloudBoardId) -> Self {
Self {
ctx,
nextcloud_board_id,
lookup: Default::default(),
}
}
}
impl Actor for LabelsActor {
type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
this.ctx
.deck_client()
.get_board(this.nextcloud_board_id)
.await
.result?
.labels
.iter()
.for_each(|l| {
this.lookup.insert(l.title.clone(), l.id);
});
Ok(())
});
}
pub(crate) struct LookupNextcloudLabelId {
pub(crate) trello_name: TrelloLabelName,
pub(crate) trello_color: TrelloLabelColor,
}
impl Message<LookupNextcloudLabelId> for LabelsActor {
type Reply = Result<NextcloudLabelId, kxio::Error>;
async fn handle(
&mut self,
msg: LookupNextcloudLabelId,
_ctx: Context<'_, Self, Self::Reply>,
) -> Result<NextcloudLabelId, kxio::Error> {
let nextcloud_label_id = match self
.lookup
.get(&NextcloudLabelTitle::new(msg.trello_name.as_ref()))
{
Some(nextcloud_label_id) => nextcloud_label_id,
None => {
p!(
self.ctx.prt,
">> Label not found in nextcloud board, creating"
);
let label = self
.ctx
.deck_client()
.create_label(
self.nextcloud_board_id,
&NextcloudLabelTitle::new(msg.trello_name.as_ref()),
&NextcloudLabelColour::new(get_color_hex(msg.trello_color.as_ref())),
)
.await
.result?;
self.lookup.insert(label.title.clone(), label.id);
self.lookup
.get(&NextcloudLabelTitle::new(msg.trello_name.as_ref()))
.expect("label was just inserted")
}
};
Ok(*nextcloud_label_id)
}
}

View file

@ -1,11 +1,12 @@
//
use std::collections::HashMap;
use color_eyre::eyre::eyre;
use kameo::actor::spawn_in_thread;
use crate::{
f,
nextcloud::model::{Label, NextcloudBoardTitle, NextcloudLabelTitle},
import::{labels::LabelsActor, stacks::ImportStacksActor, supervisor::Supervisor},
nextcloud::model::NextcloudBoardTitle,
spawn_in_thread,
trello::model::board::{TrelloBoardId, TrelloBoardName, TrelloBoards},
FullCtx,
};
@ -13,7 +14,11 @@ use crate::{
mod attachment;
mod card;
mod label;
mod labels;
mod spawn;
mod stack;
mod stacks;
mod supervisor;
pub(crate) async fn run(ctx: &FullCtx) -> color_eyre::Result<()> {
// get list of trello boards
@ -72,45 +77,26 @@ pub(crate) async fn run(ctx: &FullCtx) -> color_eyre::Result<()> {
.find(|b| b.title == nextcloud_board_name)
.map(|b| b.id)
.expect("find selected board");
// get list of nextcloud stacks in the selected board
let nextcloud_stacks = deck_client.get_stacks(nextcloud_board_id).await.result?;
stack::create_any_missing_stacks(
ctx,
&selected_trello_stack_names,
let supervisor = spawn_in_thread(Supervisor);
let labels_actor_ref = spawn_in_thread!(
supervisor,
LabelsActor::new(ctx.clone(), nextcloud_board_id)
);
let _main = spawn_in_thread!(
supervisor,
ImportStacksActor::new(
ctx.clone(),
trello_stacks.clone(),
selected_trello_stack_names.into_iter().cloned().collect(),
nextcloud_board_id,
nextcloud_stacks,
labels_actor_ref.clone(),
)
.await?;
);
// - get the list of nextcloud stacks again (with new stack ids)
let nextcloud_stacks = deck_client.get_stacks(nextcloud_board_id).await.result?;
let mut nextcloud_labels: HashMap<NextcloudLabelTitle, Label> = deck_client
.get_board(nextcloud_board_id)
.await
.result?
.labels
.iter()
.map(|l| (l.title.clone(), l.clone()))
.collect();
// for each selected trello stack
for selected_trello_stack in trello_stacks
.iter()
.filter(|s| selected_trello_stack_names.contains(&s.name.as_ref()))
.collect::<Vec<_>>()
.into_iter()
{
stack::import_stack(
ctx,
nextcloud_board_id,
&nextcloud_stacks,
&mut nextcloud_labels,
selected_trello_stack,
)
.await?;
}
supervisor.wait_for_stop().await;
Ok(())
}

25
src/import/spawn.rs Normal file
View file

@ -0,0 +1,25 @@
//
/// spawns a new actor and sets up bi-directional links
#[macro_export]
macro_rules! spawn {
($parent:expr, $actor:expr) => {{
tracing::debug!("spawning : {}", stringify!($actor));
let new_actor_ref = kameo::spawn($actor);
new_actor_ref.link(&$parent).await;
$parent.link(&new_actor_ref).await;
tracing::debug!("spawned : {}", stringify!($actor));
new_actor_ref
}};
}
#[macro_export]
macro_rules! spawn_in_thread {
($parent:expr, $actor:expr) => {{
tracing::debug!("spawning in thread : {}", stringify!($actor));
let new_actor_ref = kameo::actor::spawn_in_thread($actor);
new_actor_ref.link(&$parent).await;
$parent.link(&new_actor_ref).await;
tracing::debug!("spawned in thread : {}", stringify!($actor));
new_actor_ref
}};
}

View file

@ -1,13 +1,17 @@
//
use std::{collections::HashMap, ops::Deref};
use std::collections::HashMap;
use kameo::{
actor::{ActorID, ActorRef},
error::ActorStopReason,
mailbox::unbounded::UnboundedMailbox,
Actor,
};
use crate::{
import::card,
nextcloud::{
client::DeckClient,
model::{Label, NextcloudBoardId, NextcloudLabelTitle, Stack},
},
p,
import::{card::ImportCardActor, labels::LabelsActor},
nextcloud::model::{NextcloudBoardId, NextcloudStackId},
on_actor_link_died, on_actor_start, spawn_in_thread,
trello::{
client::TrelloClient,
model::list::{TrelloList, TrelloListId},
@ -15,68 +19,74 @@ use crate::{
FullCtx,
};
pub(super) async fn create_any_missing_stacks(
ctx: &FullCtx,
selected_trello_stack_names: &Vec<&String>,
pub(super) struct ImportStackActor {
ctx: FullCtx,
trello_stack: TrelloList,
nextcloud_board_id: NextcloudBoardId,
nextcloud_stacks: Vec<Stack>,
) -> color_eyre::Result<()> {
let deck_client: DeckClient = ctx.deck_client();
// identify any stacks by name from those selected in trello that are missing in nextcloud
let missing_stack_names = selected_trello_stack_names
.iter()
.filter(|s| !nextcloud_stacks.iter().any(|ns| ns.title.deref() == **s))
.cloned()
.collect::<Vec<_>>();
if !missing_stack_names.is_empty() {
crate::p!(ctx.prt, "Missing stacks: {:?}", missing_stack_names);
// create any missing stacks in nextcloud
// for each missing stack
for missing_stack_name in missing_stack_names.into_iter() {
// - create the stack
let stack = deck_client
.create_stack(nextcloud_board_id, &missing_stack_name.clone().into())
.await
.result?;
p!(ctx.prt, "Created stack: {}", stack.title);
nextcloud_stack_id: NextcloudStackId,
labels_actor_ref: ActorRef<LabelsActor>,
children: HashMap<ActorID, ActorRef<ImportCardActor>>,
}
p!(ctx.prt, "Stacks created");
}
Ok(())
}
pub(super) async fn import_stack(
ctx: &FullCtx,
impl ImportStackActor {
pub(crate) fn new(
ctx: FullCtx,
trello_stack: TrelloList,
nextcloud_board_id: NextcloudBoardId,
nextcloud_stacks: &Vec<Stack>,
nextcloud_labels: &mut HashMap<NextcloudLabelTitle, Label>,
selected_trello_stack: &TrelloList,
) -> color_eyre::Result<()> {
let trello_client: TrelloClient = ctx.trello_client();
nextcloud_stack_id: NextcloudStackId,
labels_actor_ref: ActorRef<LabelsActor>,
) -> Self {
Self {
ctx,
trello_stack,
nextcloud_board_id,
nextcloud_stack_id,
labels_actor_ref,
children: Default::default(),
}
}
}
impl Actor for ImportStackActor {
type Mailbox = UnboundedMailbox<Self>;
p!(ctx.prt, "Importing stack: {}", selected_trello_stack.name);
let nextcloud_stack_id = nextcloud_stacks
.iter()
.find(|ns| ns.title.deref() == selected_trello_stack.name.as_ref())
.map(|ns| ns.id)
.expect("find nextcloud stack");
on_actor_start!(this, actor_ref, {
let trello_client: TrelloClient = this.ctx.trello_client();
crate::p!(this.ctx.prt, "Importing stack: {}", this.trello_stack.name);
// - get the list of trello cards in the stack
let trello_cards = trello_client
.list_cards(&TrelloListId::new(selected_trello_stack.id.as_ref()))
.list_cards(&TrelloListId::new(this.trello_stack.id.as_ref()))
.await
.result?;
// - for each card in the trello stack
for trello_card in trello_cards.into_iter() {
card::import_card(
ctx,
nextcloud_board_id,
nextcloud_labels,
nextcloud_stack_id,
&trello_card,
let child: ActorRef<ImportCardActor> = spawn_in_thread!(
actor_ref.clone(),
ImportCardActor::new(
this.ctx.clone(),
trello_card,
this.nextcloud_board_id,
this.nextcloud_stack_id,
this.labels_actor_ref.clone(),
)
.await?;
);
this.children.insert(child.id(), child.clone());
}
Ok(())
});
on_actor_link_died!(this, actor_ref, id, reason, {
match reason {
ActorStopReason::Normal => {
this.children.remove(&id);
}
_ => {
return Ok(Some(reason));
}
}
if this.children.is_empty() {
return Ok(Some(ActorStopReason::Normal));
}
Ok(None)
});
}

150
src/import/stacks.rs Normal file
View file

@ -0,0 +1,150 @@
//
use std::{collections::HashMap, ops::Deref};
use kameo::{
actor::{ActorID, ActorRef},
error::ActorStopReason,
mailbox::unbounded::UnboundedMailbox,
Actor,
};
use crate::{
import::{labels::LabelsActor, stack::ImportStackActor},
nextcloud::{
client::DeckClient,
model::{NextcloudBoardId, Stack},
},
on_actor_link_died, on_actor_start, p, spawn_in_thread,
trello::model::list::TrelloList,
FullCtx,
};
async fn create_any_missing_stacks(
ctx: &FullCtx,
selected_trello_stack_names: &[String],
nextcloud_board_id: NextcloudBoardId,
nextcloud_stacks: Vec<Stack>,
) -> color_eyre::Result<()> {
let deck_client: DeckClient = ctx.deck_client();
// identify any stacks by name from those selected in trello that are missing in nextcloud
let missing_stack_names = selected_trello_stack_names
.iter()
.filter(|s| !nextcloud_stacks.iter().any(|ns| ns.title.deref() == *s))
.cloned()
.collect::<Vec<_>>();
if !missing_stack_names.is_empty() {
crate::p!(ctx.prt, "Missing stacks: {:?}", missing_stack_names);
// create any missing stacks in nextcloud
// for each missing stack
for missing_stack_name in missing_stack_names.into_iter() {
// - create the stack
let stack = deck_client
.create_stack(nextcloud_board_id, &missing_stack_name.clone().into())
.await
.result?;
p!(ctx.prt, "Created stack: {}", stack.title);
}
p!(ctx.prt, "Stacks created");
}
Ok(())
}
//
pub(super) struct ImportStacksActor {
ctx: FullCtx,
trello_stacks: Vec<TrelloList>,
selected_trello_stack_names: Vec<String>,
nextcloud_board_id: NextcloudBoardId,
labels_actor_ref: ActorRef<LabelsActor>,
children: HashMap<ActorID, ActorRef<ImportStackActor>>,
}
impl ImportStacksActor {
pub(super) fn new(
ctx: FullCtx,
trello_stacks: Vec<TrelloList>,
selected_trello_stack_names: Vec<String>,
nextcloud_board_id: NextcloudBoardId,
labels_actor_ref: ActorRef<LabelsActor>,
) -> Self {
Self {
ctx,
trello_stacks,
selected_trello_stack_names,
nextcloud_board_id,
labels_actor_ref,
children: Default::default(),
}
}
}
impl Actor for ImportStacksActor {
type Mailbox = UnboundedMailbox<Self>;
on_actor_start!(this, actor_ref, {
// spawn a new ImportStack actor for each trello_stack named in selected_trello_stack_names
// get list of nextcloud stacks in the selected board
let nextcloud_stacks = this
.ctx
.deck_client()
.get_stacks(this.nextcloud_board_id)
.await
.result?;
create_any_missing_stacks(
&this.ctx,
&this.selected_trello_stack_names,
this.nextcloud_board_id,
nextcloud_stacks,
)
.await?;
// - get the list of nextcloud stacks again (with new stack ids)
let nextcloud_stacks = this
.ctx
.deck_client()
.get_stacks(this.nextcloud_board_id)
.await
.result?;
// for each selected trello stack
for selected_trello_stack in this
.trello_stacks
.iter()
.filter(|s| this.selected_trello_stack_names.contains(s.name.as_ref()))
{
let nextcloud_stack_id = //this.
nextcloud_stacks
.iter()
.find(|ns| ns.title.deref() == selected_trello_stack.name.as_ref())
.map(|ns| ns.id)
.expect("find nextcloud stack");
let child: ActorRef<ImportStackActor> = spawn_in_thread!(
actor_ref,
ImportStackActor::new(
this.ctx.clone(),
selected_trello_stack.clone(),
this.nextcloud_board_id,
nextcloud_stack_id,
this.labels_actor_ref.clone(),
)
);
this.children.insert(child.id(), child);
}
Ok(())
});
on_actor_link_died!(this, actor_ref, id, reason, {
match reason {
ActorStopReason::Normal => {
this.children.remove(&id);
}
_ => {
return Ok(Some(reason));
}
}
if this.children.is_empty() {
return Ok(Some(ActorStopReason::Normal));
}
Ok(None)
});
}

22
src/import/supervisor.rs Normal file
View file

@ -0,0 +1,22 @@
//
use kameo::{
actor::{ActorID, WeakActorRef},
error::{ActorStopReason, BoxError},
mailbox::unbounded::UnboundedMailbox,
Actor,
};
pub(super) struct Supervisor;
impl Actor for Supervisor {
type Mailbox = UnboundedMailbox<Self>;
async fn on_link_died(
&mut self,
_actor_ref: WeakActorRef<Self>,
_id: ActorID,
reason: ActorStopReason,
) -> Result<Option<ActorStopReason>, BoxError> {
// normally when ImportStacksActor stops it would not cause the supervisor to stop, but we want it to stop.
Ok(Some(reason))
}
}

View file

@ -4,7 +4,11 @@ use std::path::PathBuf;
use clap::Parser;
use color_eyre::eyre::eyre;
use config::AppConfig;
use kxio::{fs::FileSystem, net::Net, print::Printer};
use kxio::{fs::FileSystem, kxeprintln as e, kxprintln as p, net::Net, print::Printer};
use crate::{nextcloud::client::DeckClient, trello::client::TrelloClient};
use execute::Execute;
mod api_result;
mod check;
@ -13,7 +17,7 @@ mod conversion;
mod execute;
mod import;
mod init;
mod macros;
pub mod macros;
mod nextcloud;
mod template;
mod trello;
@ -23,13 +27,6 @@ mod tests;
const NAME: &str = "trello-to-deck";
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!())]
pub struct Commands {

209
src/macros/actor.rs Normal file
View file

@ -0,0 +1,209 @@
//
/// Called when the actor starts, before it processes any messages.
///
/// Messages sent internally by the actor during `on_start` are prioritized and processed
/// before any externally sent messages, even if external messages are received first.
///
/// This ensures that the actor can properly initialize before handling external messages.
///
/// # Example
///
/// ```rust
/// use trello_to_deck::on_actor_start;
/// struct ServerActor;
/// impl kameo::Actor for ServerActor {
/// type Mailbox = kameo::mailbox::unbounded::UnboundedMailbox<Self>;
///
/// on_actor_start!(this, actor_ref, {
/// // handle start here
/// Ok(())
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_start {
($this:ident, $actor_ref:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_start(
&mut self,
actor_ref: kameo::actor::ActorRef<Self>,
) -> std::result::Result<(), kameo::error::BoxError> {
tracing::debug!(?actor_ref, "{}", <Self as kameo::Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_start {
($this:ident, $actor_ref:ident) => {
$crate::on_actor_start!($this, $actor_ref, { Ok(()) });
};
}
/// Called swhen the actor encounters a panic or an error during "tell" message handling.
///
/// This method gives the actor an opportunity to clean up or reset its state and determine
/// whether it should be stopped or continue processing messages.
///
/// # Parameters
/// - `err`: The panic or error that occurred.
///
/// # Returns
/// - `Some(ActorStopReason)`: Stops the actor.
/// - `None`: Allows the actor to continue processing messages.
///
/// # Example
///
/// ```rust
/// use trello_to_deck::on_actor_panic;
/// struct ServerActor;
/// impl kameo::Actor for ServerActor {
/// type Mailbox = kameo::mailbox::unbounded::UnboundedMailbox<Self>;
///
/// on_actor_panic!(this, actor_ref, err, {
/// // handle panic here
/// Ok(None)
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_panic {
($this:ident, $actor_ref:ident, $err:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_panic(
&mut self,
actor_ref: kameo::actor::WeakActorRef<Self>,
err: kameo::error::PanicError,
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
tracing::debug!(?actor_ref, %err, "{}", <Self as kameo::Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
let $err = err;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_panic {
($this:ident, $actor_ref:ident, $err:ident) => {
$crate::on_actor_panic!($this, $actor_ref, $err, {
Ok(Some(kameo::error::ActorStopReason::Panicked($err)))
});
};
}
/// Called when a linked actor dies.
///
/// By default, the actor will stop if the reason for the linked actor's death is anything other
/// than `Normal`. You can customize this behavior in the implementation.
///
/// # Returns
/// Whether the actor should stop or continue processing messages.
///
/// # Example
///
/// ```rust
/// use trello_to_deck::on_actor_link_died;
/// struct ServerActor;
/// impl kameo::Actor for ServerActor {
/// type Mailbox = kameo::mailbox::unbounded::UnboundedMailbox<Self>;
///
/// on_actor_link_died!(this, actor_ref, id, reason, {
/// // handle link death here
/// Ok(None)
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_link_died {
($this:ident, $actor_ref:ident, $id:ident, $reason:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_link_died(
&mut self,
actor_ref: kameo::actor::WeakActorRef<Self>,
id: kameo::actor::ActorID,
reason: kameo::error::ActorStopReason,
) -> std::result::Result<Option<kameo::error::ActorStopReason>, kameo::error::BoxError> {
tracing::debug!(?actor_ref, %id, %reason, "{}", <Self as kameo::Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
let $id = id;
let $reason = reason;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_link_died {
($this:ident, $actor_ref:ident, $id:ident, $reason:ident) => {
$crate::on_actor_link_died!($this, $actor_ref, $id, $reason, {
match &$reason {
kameo::error::ActorStopReason::Normal => Ok(None),
kameo::error::ActorStopReason::Killed
| kameo::error::ActorStopReason::Panicked(_)
| kameo::error::ActorStopReason::LinkDied { .. } => {
Ok(Some(kameo::error::ActorStopReason::LinkDied {
id: $id,
reason: Box::new($reason),
}))
}
}
});
};
}
/// Called before the actor stops.
///
/// This allows the actor to perform any necessary cleanup or release resources before being fully stopped.
///
/// # Parameters
/// - `reason`: The reason why the actor is being stopped.
///
/// # Example
///
/// ```rust
/// use trello_to_deck::on_actor_stop;
/// struct ServerActor;
/// impl kameo::Actor for ServerActor {
/// type Mailbox = kameo::mailbox::unbounded::UnboundedMailbox<Self>;
///
/// on_actor_stop!(this, actor_ref, reason, {
/// // handle stop here
/// Ok(())
/// });
/// }
/// ```
#[macro_export]
macro_rules! on_actor_stop {
($this:ident, $actor_ref:ident, $reason:ident, $body:expr) => {
#[allow(unused_variables)]
#[tracing::instrument(skip_all)]
async fn on_stop(
&mut self,
actor_ref: kameo::actor::WeakActorRef<Self>,
reason: kameo::error::ActorStopReason,
) -> std::result::Result<(), kameo::error::BoxError> {
tracing::debug!(?actor_ref, %reason, "{}", <Self as kameo::Actor>::name());
let $this = self;
let $actor_ref = actor_ref;
let $reason = reason;
$body
}
};
}
#[macro_export]
macro_rules! default_on_actor_stop {
($this:ident, $actor_ref:ident, $reason:ident) => {
$crate::on_actor_stop!($this, $actor_ref, $reason, { Ok(()) });
};
}

View file

@ -1,4 +1,6 @@
//
mod actor;
mod newtype;
mod print;
mod send;
mod to_string;

64
src/macros/send.rs Normal file
View file

@ -0,0 +1,64 @@
//
#[macro_export]
macro_rules! tell {
($actor_ref:expr, $message:expr) => {
tell!(stringify!($actor_ref), $actor_ref, $message)
};
($actor_name:expr, $actor_ref:expr, $message:expr) => {{
tracing::debug!(actor = $actor_name, msg = stringify!($message), "send");
$actor_ref.tell($message).await
}};
}
#[macro_export]
macro_rules! ask {
($actor_ref:expr, $message:expr) => {
ask!(stringify!($actor_ref), $actor_ref, $message)
};
($actor_name:expr, $actor_ref:expr, $message:expr) => {{
tracing::debug!(actor = $actor_name, msg = stringify!($message), "send");
$actor_ref.ask($message).await
}};
}
#[macro_export]
macro_rules! subscribe {
($message_bus:expr, $actor_ref:expr) => {
subscribe!(
stringify!($message_bus),
$message_bus,
stringify!($actor_ref),
$actor_ref
)
};
($message_bus:expr, $actor_name:expr, $actor_ref:expr) => {
subscribe!(
stringify!($message_bus),
$message_bus,
$actor_name,
$actor_ref
)
};
($bus_name:expr, $message_bus:expr, $actor_ref:expr) => {
subscribe!($bus_name, $message_bus, stringify!($actor_ref), $actor_ref)
};
($bus_name:expr, $message_bus:expr, $actor_name:expr, $actor_ref:expr) => {{
tracing::debug!(msg_bus = $bus_name, actor = $actor_name, "subscribe");
$message_bus
.tell(kameo::actor::pubsub::Subscribe($actor_ref))
.await
}};
}
#[macro_export]
macro_rules! publish {
($message_bus:expr, $message:expr) => {
publish!(stringify!($message_bus), $message_bus, $message)
};
($bus_name:expr, $message_bus:expr, $message:expr) => {{
tracing::debug!(bus = $bus_name, msg = stringify!($message), "publish");
$message_bus
.tell(kameo::actor::pubsub::Publish($message))
.await
}};
}

View file

@ -63,6 +63,7 @@ newtype!(
Hash,
PartialOrd,
Ord,
kameo::Reply,
"ID of a Nextcloud Label"
);
newtype!(