forked from NationTech/harmony
		
	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:
		
							parent
							
								
									f7dc15cbf0
								
							
						
					
					
						commit
						fc718f11cf
					
				
							
								
								
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -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" | ||||
|  | ||||
| @ -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 } | ||||
|  | ||||
| @ -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::<HAClusterTopology>::load_from_env() | ||||
|         .interpret(Box::new(lamp_stack)) | ||||
|         .await | ||||
|         .unwrap(); | ||||
| } | ||||
| 
 | ||||
| fn clone_score<T: Topology, S: Score<T> + Clone + 'static>(score: S) -> Box<S> { | ||||
|     Box::new(score.clone()) | ||||
| } | ||||
|  | ||||
| @ -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, | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -56,10 +56,7 @@ impl<T: Topology> Maestro<T> { | ||||
|         score_mut.append(&mut scores); | ||||
|     } | ||||
| 
 | ||||
|     pub async fn interpret<S>(&self, score: S) -> Result<Outcome, InterpretError> | ||||
|     where | ||||
|         S: Score<T>, | ||||
|     { | ||||
|     pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> { | ||||
|         info!("Running score {score:?}"); | ||||
|         let interpret = score.create_interpret(); | ||||
|         info!("Launching interpret {interpret:?}"); | ||||
|  | ||||
| @ -1,6 +1,21 @@ | ||||
| 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 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()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -99,7 +99,10 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret { | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> 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.ensure_hosts_registered(topology).await?; | ||||
|  | ||||
| @ -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<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 | ||||
| } | ||||
| 
 | ||||
| pub struct HarmonyTUI { | ||||
|     score: ScoreListWidget, | ||||
| pub struct HarmonyTUI<T: Topology> { | ||||
|     score: ScoreListWidget<T>, | ||||
|     should_quit: bool, | ||||
|     tui_state: TuiWidgetState, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| enum HarmonyTuiEvent { | ||||
|     LaunchScore(ScoreItem), | ||||
| enum HarmonyTuiEvent<T: Topology> { | ||||
|     LaunchScore(Box<dyn Score<T>>), | ||||
| } | ||||
| 
 | ||||
| impl HarmonyTUI { | ||||
|     pub fn new(maestro: Maestro) -> Self { | ||||
| impl<T: Topology + std::fmt::Debug + Send + Sync + 'static> HarmonyTUI<T> { | ||||
|     pub fn new(maestro: Maestro<T>) -> 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<Maestro>, | ||||
|     ) -> (tokio::task::JoinHandle<()>, mpsc::Sender<HarmonyTuiEvent>) { | ||||
|         let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent>(32); | ||||
|         maestro: Arc<Maestro<T>>, | ||||
|     ) -> ( | ||||
|         tokio::task::JoinHandle<()>, | ||||
|         mpsc::Sender<HarmonyTuiEvent<T>>, | ||||
|     ) { | ||||
|         let (sender, mut receiver) = mpsc::channel::<HarmonyTuiEvent<T>>(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<ScoreItem> { | ||||
|     fn scores_list(maestro: &Maestro<T>) -> Vec<Box<dyn Score<T>>> { | ||||
|         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<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()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<T: Topology> { | ||||
|     state: ExecutionState, | ||||
|     score: ScoreItem, | ||||
|     score: Box<dyn Score<T>>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct ScoreListWidget { | ||||
| pub(crate) struct ScoreListWidget<T: Topology> { | ||||
|     list_state: Arc<RwLock<ListState>>, | ||||
|     scores: Vec<ScoreItem>, | ||||
|     execution: Option<Execution>, | ||||
|     execution_history: Vec<Execution>, | ||||
|     sender: mpsc::Sender<HarmonyTuiEvent>, | ||||
|     scores: Vec<Box<dyn Score<T>>>, | ||||
|     execution: Option<Execution<T>>, | ||||
|     execution_history: Vec<Execution<T>>, | ||||
|     sender: mpsc::Sender<HarmonyTuiEvent<T>>, | ||||
| } | ||||
| 
 | ||||
| impl ScoreListWidget { | ||||
|     pub(crate) fn new(scores: Vec<ScoreItem>, sender: mpsc::Sender<HarmonyTuiEvent>) -> Self { | ||||
| impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> { | ||||
|     pub(crate) fn new(scores: Vec<Box<dyn Score<T>>>, sender: mpsc::Sender<HarmonyTuiEvent<T>>) -> 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<T: Topology> Widget for &ScoreListWidget<T> { | ||||
|     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<ListItem<'_>> = 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<dyn Score<T>>) -> ListItem<'a> { | ||||
|     ListItem::new(score.name()) | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user