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.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-04-02 15:51:28 -04:00
parent f7dc15cbf0
commit fc718f11cf
9 changed files with 87 additions and 64 deletions

12
Cargo.lock generated
View File

@ -823,7 +823,6 @@ dependencies = [
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_macros", "harmony_macros",
"harmony_tui",
"harmony_types", "harmony_types",
"log", "log",
"tokio", "tokio",
@ -860,6 +859,17 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "example-topology"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "example-topology2"
version = "0.1.0"
[[package]] [[package]]
name = "example-tui" name = "example-tui"
version = "0.1.0" version = "0.1.0"

View File

@ -8,7 +8,7 @@ publish = false
[dependencies] [dependencies]
harmony = { path = "../../harmony" } harmony = { path = "../../harmony" }
harmony_tui = { path = "../../harmony_tui" } #harmony_tui = { path = "../../harmony_tui" }
harmony_types = { path = "../../harmony_types" } harmony_types = { path = "../../harmony_types" }
cidr = { workspace = true } cidr = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View File

@ -2,7 +2,8 @@ use harmony::{
data::Version, data::Version,
maestro::Maestro, maestro::Maestro,
modules::lamp::{LAMPConfig, LAMPScore}, modules::lamp::{LAMPConfig, LAMPScore},
topology::Url, score::Score,
topology::{HAClusterTopology, Topology, Url},
}; };
#[tokio::main] #[tokio::main]
@ -17,8 +18,12 @@ async fn main() {
}, },
}; };
Maestro::load_from_env() Maestro::<HAClusterTopology>::load_from_env()
.interpret(Box::new(lamp_stack)) .interpret(Box::new(lamp_stack))
.await .await
.unwrap(); .unwrap();
} }
fn clone_score<T: Topology, S: Score<T> + Clone + 'static>(score: S) -> Box<S> {
Box::new(score.clone())
}

View File

@ -1,7 +1,10 @@
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore}, modules::{
dummy::{ErrorScore, PanicScore, SuccessScore},
k8s::deployment::K8sDeploymentScore,
},
topology::HAClusterTopology, topology::HAClusterTopology,
}; };

View File

@ -56,10 +56,7 @@ impl<T: Topology> Maestro<T> {
score_mut.append(&mut scores); score_mut.append(&mut scores);
} }
pub async fn interpret<S>(&self, score: S) -> Result<Outcome, InterpretError> pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
where
S: Score<T>,
{
info!("Running score {score:?}"); info!("Running score {score:?}");
let interpret = score.create_interpret(); let interpret = score.create_interpret();
info!("Launching interpret {interpret:?}"); info!("Launching interpret {interpret:?}");

View File

@ -1,6 +1,21 @@
use super::{interpret::Interpret, topology::Topology}; use super::{interpret::Interpret, topology::Topology};
pub trait Score<T: Topology>: std::fmt::Debug + Send + Sync { pub trait Score<T: Topology>: std::fmt::Debug + Send + Sync + CloneBoxScore<T> {
fn create_interpret(&self) -> Box<dyn Interpret<T>>; fn create_interpret(&self) -> Box<dyn Interpret<T>>;
fn name(&self) -> String; fn name(&self) -> String;
} }
pub trait CloneBoxScore<T: Topology> {
fn clone_box(&self) -> Box<dyn Score<T>>;
}
impl<S, T> CloneBoxScore<T> for S
where
T: Topology,
S: Score<T> + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn Score<T>> {
Box::new(self.clone())
}
}

View File

@ -99,7 +99,10 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
info!("Executing {} on inventory {inventory:?}", <DnsInterpret as Interpret<T>>::get_name(self)); info!(
"Executing {} on inventory {inventory:?}",
<DnsInterpret as Interpret<T>>::get_name(self)
);
self.serve_dhcp_entries(inventory, topology).await?; self.serve_dhcp_entries(inventory, topology).await?;
self.ensure_hosts_registered(topology).await?; self.ensure_hosts_registered(topology).await?;

View File

@ -10,12 +10,12 @@ use widget::{help::HelpWidget, score::ScoreListWidget};
use std::{panic, sync::Arc, time::Duration}; use std::{panic, sync::Arc, time::Duration};
use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind};
use harmony::{maestro::Maestro, score::Score}; use harmony::{maestro::Maestro, score::Score, topology::Topology};
use ratatui::{ use ratatui::{
self, Frame, self, Frame,
layout::{Constraint, Layout, Position}, layout::{Constraint, Layout, Position},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, ListItem}, widgets::{Block, Borders},
}; };
pub mod tui { pub mod tui {
@ -43,23 +43,25 @@ pub mod tui {
/// init(maestro).await.unwrap(); /// init(maestro).await.unwrap();
/// } /// }
/// ``` /// ```
pub async fn init(maestro: Maestro) -> Result<(), Box<dyn std::error::Error>> { pub async fn init<T: Topology + std::fmt::Debug + Send + Sync + 'static>(
maestro: Maestro<T>,
) -> Result<(), Box<dyn std::error::Error>> {
HarmonyTUI::new(maestro).init().await HarmonyTUI::new(maestro).init().await
} }
pub struct HarmonyTUI { pub struct HarmonyTUI<T: Topology> {
score: ScoreListWidget, score: ScoreListWidget<T>,
should_quit: bool, should_quit: bool,
tui_state: TuiWidgetState, tui_state: TuiWidgetState,
} }
#[derive(Debug)] #[derive(Debug)]
enum HarmonyTuiEvent { enum HarmonyTuiEvent<T: Topology> {
LaunchScore(ScoreItem), LaunchScore(Box<dyn Score<T>>),
} }
impl HarmonyTUI { impl<T: Topology + std::fmt::Debug + Send + Sync + 'static> HarmonyTUI<T> {
pub fn new(maestro: Maestro) -> Self { pub fn new(maestro: Maestro<T>) -> Self {
let maestro = Arc::new(maestro); let maestro = Arc::new(maestro);
let (_handle, sender) = Self::start_channel(maestro.clone()); let (_handle, sender) = Self::start_channel(maestro.clone());
let score = ScoreListWidget::new(Self::scores_list(&maestro), sender); let score = ScoreListWidget::new(Self::scores_list(&maestro), sender);
@ -72,9 +74,12 @@ impl HarmonyTUI {
} }
fn start_channel( fn start_channel(
maestro: Arc<Maestro>, maestro: Arc<Maestro<T>>,
) -> (tokio::task::JoinHandle<()>, mpsc::Sender<HarmonyTuiEvent>) { ) -> (
let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent>(32); tokio::task::JoinHandle<()>,
mpsc::Sender<HarmonyTuiEvent<T>>,
) {
let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent<T>>(32);
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
info!("Starting message channel receiver loop"); info!("Starting message channel receiver loop");
while let Some(event) = receiver.recv().await { while let Some(event) = receiver.recv().await {
@ -84,8 +89,7 @@ impl HarmonyTUI {
let maestro = maestro.clone(); let maestro = maestro.clone();
let joinhandle_result = let joinhandle_result =
tokio::spawn(async move { maestro.interpret(score_item.0).await }) tokio::spawn(async move { maestro.interpret(score_item).await }).await;
.await;
match joinhandle_result { match joinhandle_result {
Ok(interpretation_result) => match interpretation_result { Ok(interpretation_result) => match interpretation_result {
@ -163,13 +167,10 @@ impl HarmonyTUI {
frame.render_widget(tui_logger, output_area) frame.render_widget(tui_logger, output_area)
} }
fn scores_list(maestro: &Maestro) -> Vec<ScoreItem> { fn scores_list(maestro: &Maestro<T>) -> Vec<Box<dyn Score<T>>> {
let scores = maestro.scores(); let scores = maestro.scores();
let scores_read = scores.read().expect("Should be able to read scores"); let scores_read = scores.read().expect("Should be able to read scores");
scores_read scores_read.iter().map(|s| s.clone_box()).collect()
.iter()
.map(|s| ScoreItem(s.clone_box()))
.collect()
} }
async fn handle_event(&mut self, event: &Event) { async fn handle_event(&mut self, event: &Event) {
@ -189,18 +190,3 @@ impl HarmonyTUI {
} }
} }
} }
#[derive(Debug)]
struct ScoreItem(Box<dyn Score>);
impl ScoreItem {
pub fn clone(&self) -> Self {
Self(self.0.clone_box())
}
}
impl Into<ListItem<'_>> for &ScoreItem {
fn into(self) -> ListItem<'static> {
ListItem::new(self.0.name())
}
}

View File

@ -1,16 +1,14 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crossterm::event::{Event, KeyCode, KeyEventKind}; use crossterm::event::{Event, KeyCode, KeyEventKind};
use harmony::{score::Score, topology::Topology};
use log::{info, warn}; use log::{info, warn};
use ratatui::{ use ratatui::{
Frame, layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame
layout::Rect,
style::{Style, Stylize},
widgets::{List, ListState, StatefulWidget, Widget},
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::{HarmonyTuiEvent, ScoreItem}; use crate::HarmonyTuiEvent;
#[derive(Debug)] #[derive(Debug)]
enum ExecutionState { enum ExecutionState {
@ -20,22 +18,22 @@ enum ExecutionState {
} }
#[derive(Debug)] #[derive(Debug)]
struct Execution { struct Execution<T: Topology> {
state: ExecutionState, state: ExecutionState,
score: ScoreItem, score: Box<dyn Score<T>>,
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct ScoreListWidget { pub(crate) struct ScoreListWidget<T: Topology> {
list_state: Arc<RwLock<ListState>>, list_state: Arc<RwLock<ListState>>,
scores: Vec<ScoreItem>, scores: Vec<Box<dyn Score<T>>>,
execution: Option<Execution>, execution: Option<Execution<T>>,
execution_history: Vec<Execution>, execution_history: Vec<Execution<T>>,
sender: mpsc::Sender<HarmonyTuiEvent>, sender: mpsc::Sender<HarmonyTuiEvent<T>>,
} }
impl ScoreListWidget { impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> {
pub(crate) fn new(scores: Vec<ScoreItem>, sender: mpsc::Sender<HarmonyTuiEvent>) -> Self { pub(crate) fn new(scores: Vec<Box<dyn Score<T>>>, sender: mpsc::Sender<HarmonyTuiEvent<T>>) -> Self {
let mut list_state = ListState::default(); let mut list_state = ListState::default();
list_state.select_first(); list_state.select_first();
let list_state = Arc::new(RwLock::new(list_state)); let list_state = Arc::new(RwLock::new(list_state));
@ -58,9 +56,9 @@ impl ScoreListWidget {
self.execution = Some(Execution { self.execution = Some(Execution {
state: ExecutionState::INITIATED, 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 { } else {
warn!("No Score selected, nothing to launch"); warn!("No Score selected, nothing to launch");
} }
@ -94,7 +92,7 @@ impl ScoreListWidget {
execution.state = ExecutionState::RUNNING; execution.state = ExecutionState::RUNNING;
info!("Launch execution {:?}", execution); info!("Launch execution {:?}", execution);
self.sender self.sender
.send(HarmonyTuiEvent::LaunchScore(execution.score.clone())) .send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box()))
.await .await
.expect("Should be able to send message"); .expect("Should be able to send message");
} }
@ -123,16 +121,22 @@ impl ScoreListWidget {
} }
} }
impl Widget for &ScoreListWidget { impl<T: Topology> Widget for &ScoreListWidget<T> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where where
Self: Sized, Self: Sized,
{ {
let mut list_state = self.list_state.write().unwrap(); let mut list_state = self.list_state.write().unwrap();
let list = List::new(&self.scores) let scores_items: Vec<ListItem<'_>> = self.scores.iter().map(score_to_list_item).collect();
let list = List::new(scores_items)
.highlight_style(Style::new().bold().italic()) .highlight_style(Style::new().bold().italic())
.highlight_symbol("🠊 "); .highlight_symbol("🠊 ");
StatefulWidget::render(list, area, buf, &mut list_state) StatefulWidget::render(list, area, buf, &mut list_state)
} }
} }
fn score_to_list_item<'a, T: Topology>(score: &'a Box<dyn Score<T>>) -> ListItem<'a> {
ListItem::new(score.name())
}