From 0e123898db2fc2fb7e9e0e3dc2f1f2fd9ef2fe19 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Fri, 20 Dec 2024 21:01:33 +0000 Subject: [PATCH] feat: split into actors --- Cargo.toml | 1 + src/import/attachment.rs | 167 +++++++++++++++++++-------- src/import/card.rs | 239 +++++++++++++++++++++++++++++---------- src/import/label.rs | 108 +++++++++--------- src/import/labels.rs | 142 +++++++++++++++++++++++ src/import/mod.rs | 60 ++++------ src/import/spawn.rs | 25 ++++ src/import/stack.rs | 140 ++++++++++++----------- src/import/stacks.rs | 150 ++++++++++++++++++++++++ src/import/supervisor.rs | 22 ++++ src/lib.rs | 15 +-- src/macros/actor.rs | 209 ++++++++++++++++++++++++++++++++++ src/macros/mod.rs | 2 + src/macros/send.rs | 64 +++++++++++ src/nextcloud/model.rs | 1 + 15 files changed, 1070 insertions(+), 275 deletions(-) create mode 100644 src/import/labels.rs create mode 100644 src/import/spawn.rs create mode 100644 src/import/stacks.rs create mode 100644 src/import/supervisor.rs create mode 100644 src/macros/actor.rs create mode 100644 src/macros/send.rs diff --git a/Cargo.toml b/Cargo.toml index 36a12c5..3a5c4e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"]} diff --git a/src/import/attachment.rs b/src/import/attachment.rs index 03d7662..6e19ba6 100644 --- a/src/import/attachment.rs +++ b/src/import/attachment.rs @@ -1,62 +1,131 @@ // use std::path::PathBuf; +use kameo::{mailbox::unbounded::UnboundedMailbox, Actor}; + use crate::{ - nextcloud::{ - client::DeckClient, - model::{Card, NextcloudBoardId, NextcloudStackId}, - }, - p, - trello::{ - client::TrelloClient, - model::{ - attachment::{TrelloAttachment, TrelloAttachmentId}, - card::TrelloShortCard, - }, + nextcloud::model::{NextcloudBoardId, NextcloudCardId, NextcloudStackId}, + on_actor_start, p, + trello::model::{ + attachment::{TrelloAttachment, TrelloAttachmentId}, + 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, - &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(()) + nextcloud_card_id, + trello_card_id, + trello_attachment, + } + } +} +impl Actor for ImportAttachmentActor { + type Mailbox = UnboundedMailbox; + + 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!( + this.ctx.prt, + ">> Attachment added: {}:{}", + attachment.id, + attachment.attachment_type, + // attachment.extended_data.mimetype + ); + // delete local copy of attachment + attachment_file.remove()?; + + Ok(actor_ref.stop_gracefully().await?) + }); } diff --git a/src/import/card.rs b/src/import/card.rs index f515da5..f201338 100644 --- a/src/import/card.rs +++ b/src/import/card.rs @@ -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, +// 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 = 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, nextcloud_stack_id: NextcloudStackId, - trello_card: &TrelloShortCard, -) -> color_eyre::Result<()> { - let trello_client: TrelloClient = ctx.trello_client(); + labels_actor_ref: ActorRef, - 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 = 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() { - label::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(()) + labels_children: HashMap>, + attachments_children: HashMap>, +} +impl ImportCardActor { + pub(crate) fn new( + ctx: FullCtx, + trello_card: TrelloShortCard, + nextcloud_board_id: NextcloudBoardId, + nextcloud_stack_id: NextcloudStackId, + labels_actor_ref: ActorRef, + ) -> Self { + Self { + ctx, + trello_card, + nextcloud_board_id, + nextcloud_stack_id, + labels_actor_ref, + + labels_children: Default::default(), + attachments_children: Default::default(), + } + } +} +impl Actor for ImportCardActor { + type Mailbox = UnboundedMailbox; + + 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(&this.trello_card.name); + let desc: Option = match this.trello_card.desc.len() { + 0 => None, + _ => Some(NextcloudCardDescription::from(&this.trello_card.desc)), + }; + let nextcloud_card = deck_client + .create_card( + this.nextcloud_board_id, + this.nextcloud_stack_id, + &title, + desc.as_ref(), + ) + .await + .result?; + + // - - for each label on the trello 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(), + ) + ); + this.labels_children.insert(child.id(), child.clone()); + } + + let attachments = trello_client + .card(&this.trello_card.id) + .await + .result? + .attachments; + 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, + ) + ); + 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) + }); } diff --git a/src/import/label.rs b/src/import/label.rs index c3933d3..8d493e2 100644 --- a/src/import/label.rs +++ b/src/import/label.rs @@ -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, 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_card_id: NextcloudCardId, + trello_label: TrelloLabel, + labels_actor_ref: ActorRef, +} +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, + ) -> Self { + Self { + ctx, nextcloud_board_id, nextcloud_stack_id, - nextcloud_card.id, - nextcloud_label.id, - ) - .await - .result?; - Ok(()) + nextcloud_card_id, + trello_label, + labels_actor_ref, + } + } +} +impl Actor for ImportLabelActor { + type Mailbox = UnboundedMailbox; + + 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(actor_ref.stop_gracefully().await?) + }); } diff --git a/src/import/labels.rs b/src/import/labels.rs new file mode 100644 index 0000000..6aa622a --- /dev/null +++ b/src/import/labels.rs @@ -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, +// 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, +} +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; + + 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 for LabelsActor { + type Reply = Result; + async fn handle( + &mut self, + msg: LookupNextcloudLabelId, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Result { + 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) + } +} diff --git a/src/import/mod.rs b/src/import/mod.rs index 302a7a5..bec9c0c 100644 --- a/src/import/mod.rs +++ b/src/import/mod.rs @@ -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, - nextcloud_board_id, - nextcloud_stacks, - ) - .await?; + let supervisor = spawn_in_thread(Supervisor); - // - get the list of nextcloud stacks again (with new stack ids) - let nextcloud_stacks = deck_client.get_stacks(nextcloud_board_id).await.result?; + let labels_actor_ref = spawn_in_thread!( + supervisor, + LabelsActor::new(ctx.clone(), nextcloud_board_id) + ); - let mut nextcloud_labels: HashMap = 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::>() - .into_iter() - { - stack::import_stack( - ctx, + 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, - &mut nextcloud_labels, - selected_trello_stack, + labels_actor_ref.clone(), ) - .await?; - } + ); + + supervisor.wait_for_stop().await; Ok(()) } diff --git a/src/import/spawn.rs b/src/import/spawn.rs new file mode 100644 index 0000000..a679a45 --- /dev/null +++ b/src/import/spawn.rs @@ -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 + }}; +} diff --git a/src/import/stack.rs b/src/import/stack.rs index a66293a..ef1757b 100644 --- a/src/import/stack.rs +++ b/src/import/stack.rs @@ -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, -) -> 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::>(); - 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(()) + nextcloud_stack_id: NextcloudStackId, + labels_actor_ref: ActorRef, + children: HashMap>, } - -pub(super) async fn import_stack( - ctx: &FullCtx, - nextcloud_board_id: NextcloudBoardId, - nextcloud_stacks: &Vec, - nextcloud_labels: &mut HashMap, - selected_trello_stack: &TrelloList, -) -> color_eyre::Result<()> { - let trello_client: TrelloClient = ctx.trello_client(); - - 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"); - // - get the list of trello cards in the stack - let trello_cards = trello_client - .list_cards(&TrelloListId::new(selected_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( +impl ImportStackActor { + pub(crate) fn new( + ctx: FullCtx, + trello_stack: TrelloList, + nextcloud_board_id: NextcloudBoardId, + nextcloud_stack_id: NextcloudStackId, + labels_actor_ref: ActorRef, + ) -> Self { + Self { ctx, + trello_stack, nextcloud_board_id, - nextcloud_labels, nextcloud_stack_id, - &trello_card, - ) - .await?; + labels_actor_ref, + children: Default::default(), + } } - Ok(()) +} +impl Actor for ImportStackActor { + type Mailbox = UnboundedMailbox; + + 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(this.trello_stack.id.as_ref())) + .await + .result?; + + // - for each card in the trello stack + for trello_card in trello_cards.into_iter() { + let child: ActorRef = 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(), + ) + ); + 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) + }); } diff --git a/src/import/stacks.rs b/src/import/stacks.rs new file mode 100644 index 0000000..0f217b5 --- /dev/null +++ b/src/import/stacks.rs @@ -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, +) -> 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::>(); + 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, + selected_trello_stack_names: Vec, + nextcloud_board_id: NextcloudBoardId, + labels_actor_ref: ActorRef, + + children: HashMap>, +} +impl ImportStacksActor { + pub(super) fn new( + ctx: FullCtx, + trello_stacks: Vec, + selected_trello_stack_names: Vec, + nextcloud_board_id: NextcloudBoardId, + labels_actor_ref: ActorRef, + ) -> 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; + + 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 = 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) + }); +} diff --git a/src/import/supervisor.rs b/src/import/supervisor.rs new file mode 100644 index 0000000..72fb81e --- /dev/null +++ b/src/import/supervisor.rs @@ -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; + + async fn on_link_died( + &mut self, + _actor_ref: WeakActorRef, + _id: ActorID, + reason: ActorStopReason, + ) -> Result, BoxError> { + // normally when ImportStacksActor stops it would not cause the supervisor to stop, but we want it to stop. + Ok(Some(reason)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 295d549..0ba2002 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { diff --git a/src/macros/actor.rs b/src/macros/actor.rs new file mode 100644 index 0000000..c0975d5 --- /dev/null +++ b/src/macros/actor.rs @@ -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; +/// +/// 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, + ) -> std::result::Result<(), kameo::error::BoxError> { + tracing::debug!(?actor_ref, "{}", ::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; +/// +/// 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, + err: kameo::error::PanicError, + ) -> std::result::Result, kameo::error::BoxError> { + tracing::debug!(?actor_ref, %err, "{}", ::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; +/// +/// 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, + id: kameo::actor::ActorID, + reason: kameo::error::ActorStopReason, + ) -> std::result::Result, kameo::error::BoxError> { + tracing::debug!(?actor_ref, %id, %reason, "{}", ::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; +/// +/// 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, + reason: kameo::error::ActorStopReason, + ) -> std::result::Result<(), kameo::error::BoxError> { + tracing::debug!(?actor_ref, %reason, "{}", ::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(()) }); + }; +} diff --git a/src/macros/mod.rs b/src/macros/mod.rs index bdb5d51..12606ff 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -1,4 +1,6 @@ // +mod actor; mod newtype; mod print; +mod send; mod to_string; diff --git a/src/macros/send.rs b/src/macros/send.rs new file mode 100644 index 0000000..583fa49 --- /dev/null +++ b/src/macros/send.rs @@ -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 + }}; +} diff --git a/src/nextcloud/model.rs b/src/nextcloud/model.rs index 17f480f..3c9a8e6 100644 --- a/src/nextcloud/model.rs +++ b/src/nextcloud/model.rs @@ -63,6 +63,7 @@ newtype!( Hash, PartialOrd, Ord, + kameo::Reply, "ID of a Nextcloud Label" ); newtype!(