From fc718f11cfa60e1e220c2b655024406abcc79f62 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 2 Apr 2025 15:51:28 -0400 Subject: [PATCH] feat: Introduce Topology Trait for Compile-Time Safe Score Binding Introduce the `Topology` trait to ensure that `Maestro` can compile-time safely bind compatible `Scores` and `Topologies`. This refactoring includes updating `HarmonyTuiEvent`, `ScoreListWidget`, and related structures to work with generic `Topology` types, enhancing type safety and modularity. --- Cargo.lock | 12 ++++++- examples/lamp/Cargo.toml | 2 +- examples/lamp/src/main.rs | 9 ++++-- examples/tui/src/main.rs | 5 ++- harmony/src/domain/maestro/mod.rs | 5 +-- harmony/src/domain/score.rs | 17 +++++++++- harmony/src/modules/dns.rs | 5 ++- harmony_tui/src/lib.rs | 54 ++++++++++++------------------- harmony_tui/src/widget/score.rs | 42 +++++++++++++----------- 9 files changed, 87 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2cad7e9..d65658c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,7 +823,6 @@ dependencies = [ "env_logger", "harmony", "harmony_macros", - "harmony_tui", "harmony_types", "log", "tokio", @@ -860,6 +859,17 @@ dependencies = [ "url", ] +[[package]] +name = "example-topology" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "example-topology2" +version = "0.1.0" + [[package]] name = "example-tui" version = "0.1.0" diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml index 902548e..1bdcf68 100644 --- a/examples/lamp/Cargo.toml +++ b/examples/lamp/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -harmony_tui = { path = "../../harmony_tui" } +#harmony_tui = { path = "../../harmony_tui" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 1643511..1251b8c 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -2,7 +2,8 @@ use harmony::{ data::Version, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, - topology::Url, + score::Score, + topology::{HAClusterTopology, Topology, Url}, }; #[tokio::main] @@ -17,8 +18,12 @@ async fn main() { }, }; - Maestro::load_from_env() + Maestro::::load_from_env() .interpret(Box::new(lamp_stack)) .await .unwrap(); } + +fn clone_score + Clone + 'static>(score: S) -> Box { + Box::new(score.clone()) +} diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 3a683d0..9623fa2 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -1,7 +1,10 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore}, + modules::{ + dummy::{ErrorScore, PanicScore, SuccessScore}, + k8s::deployment::K8sDeploymentScore, + }, topology::HAClusterTopology, }; diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 8bb369b..256c759 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -56,10 +56,7 @@ impl Maestro { score_mut.append(&mut scores); } - pub async fn interpret(&self, score: S) -> Result - where - S: Score, - { + pub async fn interpret(&self, score: Box>) -> Result { info!("Running score {score:?}"); let interpret = score.create_interpret(); info!("Launching interpret {interpret:?}"); diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 672c1e5..6e4eed4 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,6 +1,21 @@ use super::{interpret::Interpret, topology::Topology}; -pub trait Score: std::fmt::Debug + Send + Sync { +pub trait Score: std::fmt::Debug + Send + Sync + CloneBoxScore { fn create_interpret(&self) -> Box>; fn name(&self) -> String; } + +pub trait CloneBoxScore { + fn clone_box(&self) -> Box>; +} + + +impl CloneBoxScore for S +where + T: Topology, + S: Score + Clone + 'static, +{ + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index ab4e3ec..c96d08e 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -99,7 +99,10 @@ impl Interpret for DnsInterpret { inventory: &Inventory, topology: &T, ) -> Result { - info!("Executing {} on inventory {inventory:?}", >::get_name(self)); + info!( + "Executing {} on inventory {inventory:?}", + >::get_name(self) + ); self.serve_dhcp_entries(inventory, topology).await?; self.ensure_hosts_registered(topology).await?; diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 7ee0301..10997ad 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -10,12 +10,12 @@ use widget::{help::HelpWidget, score::ScoreListWidget}; use std::{panic, sync::Arc, time::Duration}; use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; -use harmony::{maestro::Maestro, score::Score}; +use harmony::{maestro::Maestro, score::Score, topology::Topology}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, style::{Color, Style}, - widgets::{Block, Borders, ListItem}, + widgets::{Block, Borders}, }; pub mod tui { @@ -43,23 +43,25 @@ pub mod tui { /// init(maestro).await.unwrap(); /// } /// ``` -pub async fn init(maestro: Maestro) -> Result<(), Box> { +pub async fn init( + maestro: Maestro, +) -> Result<(), Box> { HarmonyTUI::new(maestro).init().await } -pub struct HarmonyTUI { - score: ScoreListWidget, +pub struct HarmonyTUI { + score: ScoreListWidget, should_quit: bool, tui_state: TuiWidgetState, } #[derive(Debug)] -enum HarmonyTuiEvent { - LaunchScore(ScoreItem), +enum HarmonyTuiEvent { + LaunchScore(Box>), } -impl HarmonyTUI { - pub fn new(maestro: Maestro) -> Self { +impl HarmonyTUI { + pub fn new(maestro: Maestro) -> Self { let maestro = Arc::new(maestro); let (_handle, sender) = Self::start_channel(maestro.clone()); let score = ScoreListWidget::new(Self::scores_list(&maestro), sender); @@ -72,9 +74,12 @@ impl HarmonyTUI { } fn start_channel( - maestro: Arc, - ) -> (tokio::task::JoinHandle<()>, mpsc::Sender) { - let (sender, mut receiver) = mpsc::channel::(32); + maestro: Arc>, + ) -> ( + tokio::task::JoinHandle<()>, + mpsc::Sender>, + ) { + let (sender, mut receiver) = mpsc::channel::>(32); let handle = tokio::spawn(async move { info!("Starting message channel receiver loop"); while let Some(event) = receiver.recv().await { @@ -84,8 +89,7 @@ impl HarmonyTUI { let maestro = maestro.clone(); let joinhandle_result = - tokio::spawn(async move { maestro.interpret(score_item.0).await }) - .await; + tokio::spawn(async move { maestro.interpret(score_item).await }).await; match joinhandle_result { Ok(interpretation_result) => match interpretation_result { @@ -163,13 +167,10 @@ impl HarmonyTUI { frame.render_widget(tui_logger, output_area) } - fn scores_list(maestro: &Maestro) -> Vec { + fn scores_list(maestro: &Maestro) -> Vec>> { let scores = maestro.scores(); let scores_read = scores.read().expect("Should be able to read scores"); - scores_read - .iter() - .map(|s| ScoreItem(s.clone_box())) - .collect() + scores_read.iter().map(|s| s.clone_box()).collect() } async fn handle_event(&mut self, event: &Event) { @@ -189,18 +190,3 @@ impl HarmonyTUI { } } } - -#[derive(Debug)] -struct ScoreItem(Box); - -impl ScoreItem { - pub fn clone(&self) -> Self { - Self(self.0.clone_box()) - } -} - -impl Into> for &ScoreItem { - fn into(self) -> ListItem<'static> { - ListItem::new(self.0.name()) - } -} diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index af992f7..514ab79 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,16 +1,14 @@ use std::sync::{Arc, RwLock}; use crossterm::event::{Event, KeyCode, KeyEventKind}; +use harmony::{score::Score, topology::Topology}; use log::{info, warn}; use ratatui::{ - Frame, - layout::Rect, - style::{Style, Stylize}, - widgets::{List, ListState, StatefulWidget, Widget}, + layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame }; use tokio::sync::mpsc; -use crate::{HarmonyTuiEvent, ScoreItem}; +use crate::HarmonyTuiEvent; #[derive(Debug)] enum ExecutionState { @@ -20,22 +18,22 @@ enum ExecutionState { } #[derive(Debug)] -struct Execution { +struct Execution { state: ExecutionState, - score: ScoreItem, + score: Box>, } #[derive(Debug)] -pub(crate) struct ScoreListWidget { +pub(crate) struct ScoreListWidget { list_state: Arc>, - scores: Vec, - execution: Option, - execution_history: Vec, - sender: mpsc::Sender, + scores: Vec>>, + execution: Option>, + execution_history: Vec>, + sender: mpsc::Sender>, } -impl ScoreListWidget { - pub(crate) fn new(scores: Vec, sender: mpsc::Sender) -> Self { +impl ScoreListWidget { + pub(crate) fn new(scores: Vec>>, sender: mpsc::Sender>) -> Self { let mut list_state = ListState::default(); list_state.select_first(); let list_state = Arc::new(RwLock::new(list_state)); @@ -58,9 +56,9 @@ impl ScoreListWidget { self.execution = Some(Execution { state: ExecutionState::INITIATED, - score: score.clone(), + score: score.clone_box(), }); - info!("{:#?}\n\nConfirm Execution (Press y/n)", score.0); + info!("{:#?}\n\nConfirm Execution (Press y/n)", score); } else { warn!("No Score selected, nothing to launch"); } @@ -94,7 +92,7 @@ impl ScoreListWidget { execution.state = ExecutionState::RUNNING; info!("Launch execution {:?}", execution); self.sender - .send(HarmonyTuiEvent::LaunchScore(execution.score.clone())) + .send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box())) .await .expect("Should be able to send message"); } @@ -123,16 +121,22 @@ impl ScoreListWidget { } } -impl Widget for &ScoreListWidget { +impl Widget for &ScoreListWidget { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let mut list_state = self.list_state.write().unwrap(); - let list = List::new(&self.scores) + let scores_items: Vec> = self.scores.iter().map(score_to_list_item).collect(); + let list = List::new(scores_items) .highlight_style(Style::new().bold().italic()) .highlight_symbol("🠊 "); StatefulWidget::render(list, area, buf, &mut list_state) } } + +fn score_to_list_item<'a, T: Topology>(score: &'a Box>) -> ListItem<'a> { + ListItem::new(score.name()) +} +