1use cu29_traits::{CuError, CuResult};
7use html_escape::encode_text;
8use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
9use petgraph::visit::EdgeRef;
10pub use petgraph::Direction::Incoming;
11pub use petgraph::Direction::Outgoing;
12use ron::extensions::Extensions;
13use ron::value::Value as RonValue;
14use ron::{Number, Options};
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use std::collections::HashMap;
17use std::fmt;
18use std::fmt::Display;
19use std::fs::read_to_string;
20use ConfigGraphs::{Missions, Simple};
21
22pub type NodeId = u32;
25
26#[derive(Serialize, Deserialize, Debug, Clone, Default)]
30pub struct ComponentConfig(pub HashMap<String, Value>);
31
32impl Display for ComponentConfig {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 let mut first = true;
35 let ComponentConfig(config) = self;
36 write!(f, "{{")?;
37 for (key, value) in config.iter() {
38 if !first {
39 write!(f, ", ")?;
40 }
41 write!(f, "{key}: {value}")?;
42 first = false;
43 }
44 write!(f, "}}")
45 }
46}
47
48impl ComponentConfig {
50 #[allow(dead_code)]
51 pub fn new() -> Self {
52 ComponentConfig(HashMap::new())
53 }
54
55 #[allow(dead_code)]
56 pub fn get<T: From<Value>>(&self, key: &str) -> Option<T> {
57 let ComponentConfig(config) = self;
58 config.get(key).map(|v| T::from(v.clone()))
59 }
60
61 #[allow(dead_code)]
62 pub fn set<T: Into<Value>>(&mut self, key: &str, value: T) {
63 let ComponentConfig(config) = self;
64 config.insert(key.to_string(), value.into());
65 }
66}
67
68#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
77pub struct Value(RonValue);
78
79macro_rules! impl_from_numeric_for_value {
81 ($($source:ty),* $(,)?) => {
82 $(impl From<$source> for Value {
83 fn from(value: $source) -> Self {
84 Value(RonValue::Number(value.into()))
85 }
86 })*
87 };
88}
89
90impl_from_numeric_for_value!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
92
93impl From<Value> for bool {
94 fn from(value: Value) -> Self {
95 if let Value(RonValue::Bool(v)) = value {
96 v
97 } else {
98 panic!("Expected a Boolean variant but got {value:?}")
99 }
100 }
101}
102macro_rules! impl_from_value_for_int {
103 ($($target:ty),* $(,)?) => {
104 $(
105 impl From<Value> for $target {
106 fn from(value: Value) -> Self {
107 if let Value(RonValue::Number(num)) = value {
108 match num {
109 Number::I8(n) => n as $target,
110 Number::I16(n) => n as $target,
111 Number::I32(n) => n as $target,
112 Number::I64(n) => n as $target,
113 Number::U8(n) => n as $target,
114 Number::U16(n) => n as $target,
115 Number::U32(n) => n as $target,
116 Number::U64(n) => n as $target,
117 Number::F32(_) | Number::F64(_) => {
118 panic!("Expected an integer Number variant but got {num:?}")
119 }
120 }
121 } else {
122 panic!("Expected a Number variant but got {value:?}")
123 }
124 }
125 }
126 )*
127 };
128}
129
130impl_from_value_for_int!(u8, i8, u16, i16, u32, i32, u64, i64);
131
132impl From<Value> for f64 {
133 fn from(value: Value) -> Self {
134 if let Value(RonValue::Number(num)) = value {
135 num.into_f64()
136 } else {
137 panic!("Expected a Number variant but got {value:?}")
138 }
139 }
140}
141
142impl From<String> for Value {
143 fn from(value: String) -> Self {
144 Value(RonValue::String(value))
145 }
146}
147
148impl From<Value> for String {
149 fn from(value: Value) -> Self {
150 if let Value(RonValue::String(s)) = value {
151 s
152 } else {
153 panic!("Expected a String variant")
154 }
155 }
156}
157
158impl Display for Value {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 let Value(value) = self;
161 match value {
162 RonValue::Number(n) => {
163 let s = match n {
164 Number::I8(n) => n.to_string(),
165 Number::I16(n) => n.to_string(),
166 Number::I32(n) => n.to_string(),
167 Number::I64(n) => n.to_string(),
168 Number::U8(n) => n.to_string(),
169 Number::U16(n) => n.to_string(),
170 Number::U32(n) => n.to_string(),
171 Number::U64(n) => n.to_string(),
172 Number::F32(n) => n.0.to_string(),
173 Number::F64(n) => n.0.to_string(),
174 _ => panic!("Expected a Number variant but got {value:?}"),
175 };
176 write!(f, "{s}")
177 }
178 RonValue::String(s) => write!(f, "{s}"),
179 RonValue::Bool(b) => write!(f, "{b}"),
180 RonValue::Map(m) => write!(f, "{m:?}"),
181 RonValue::Char(c) => write!(f, "{c:?}"),
182 RonValue::Unit => write!(f, "unit"),
183 RonValue::Option(o) => write!(f, "{o:?}"),
184 RonValue::Seq(s) => write!(f, "{s:?}"),
185 RonValue::Bytes(bytes) => write!(f, "{bytes:?}"),
186 }
187 }
188}
189
190#[derive(Serialize, Deserialize, Debug, Clone)]
193pub struct Node {
194 id: String,
195
196 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
197 type_: Option<String>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
200 config: Option<ComponentConfig>,
201
202 missions: Option<Vec<String>>,
203}
204
205impl Node {
206 #[allow(dead_code)]
207 pub fn new(id: &str, ptype: &str) -> Self {
208 Node {
209 id: id.to_string(),
210 type_: Some(ptype.to_string()),
211 config: None,
213 missions: None,
214 }
215 }
216
217 #[allow(dead_code)]
218 pub fn get_id(&self) -> String {
219 self.id.clone()
220 }
221
222 #[allow(dead_code)]
223 pub fn set_type(mut self, name: Option<String>) -> Self {
224 self.type_ = name;
225 self
226 }
227
228 #[allow(dead_code)]
229 pub fn get_type(&self) -> &str {
230 self.type_.as_ref().unwrap()
231 }
232
233 #[allow(dead_code)]
234 pub fn get_instance_config(&self) -> Option<&ComponentConfig> {
235 self.config.as_ref()
236 }
237
238 #[allow(dead_code)]
239 pub fn get_param<T: From<Value>>(&self, key: &str) -> Option<T> {
240 let pc = self.config.as_ref()?;
241 let ComponentConfig(pc) = pc;
242 let v = pc.get(key)?;
243 Some(T::from(v.clone()))
244 }
245
246 #[allow(dead_code)]
247 pub fn set_param<T: Into<Value>>(&mut self, key: &str, value: T) {
248 if self.config.is_none() {
249 self.config = Some(ComponentConfig(HashMap::new()));
250 }
251 let ComponentConfig(config) = self.config.as_mut().unwrap();
252 config.insert(key.to_string(), value.into());
253 }
254}
255
256#[derive(Serialize, Deserialize, Debug, Clone)]
258pub struct Cnx {
259 src: String,
261
262 dst: String,
264
265 pub msg: String,
267
268 pub missions: Option<Vec<String>>,
270
271 pub store: Option<bool>,
273}
274
275#[derive(Default, Debug, Clone)]
276pub struct CuGraph(pub StableDiGraph<Node, Cnx, NodeId>);
277
278impl CuGraph {
279 #[allow(dead_code)]
280 pub fn get_all_nodes(&self) -> Vec<(NodeId, &Node)> {
281 self.0
282 .node_indices()
283 .map(|index| (index.index() as u32, &self.0[index]))
284 .collect()
285 }
286
287 pub fn node_indices(&self) -> Vec<petgraph::stable_graph::NodeIndex> {
288 self.0.node_indices().collect()
289 }
290
291 pub fn add_node(&mut self, node: Node) -> CuResult<NodeId> {
292 Ok(self.0.add_node(node).index() as NodeId)
293 }
294
295 pub fn connect_ext(
296 &mut self,
297 source: NodeId,
298 target: NodeId,
299 msg_type: &str,
300 store: Option<bool>,
301 missions: Option<Vec<String>>,
302 ) -> CuResult<()> {
303 let (src_id, dst_id) = (
304 self.0
305 .node_weight(source.into())
306 .ok_or("Source node not found")?
307 .id
308 .clone(),
309 self.0
310 .node_weight(target.into())
311 .ok_or("Target node not found")?
312 .id
313 .clone(),
314 );
315
316 let _ = self.0.add_edge(
317 petgraph::stable_graph::NodeIndex::from(source),
318 petgraph::stable_graph::NodeIndex::from(target),
319 Cnx {
320 src: src_id,
321 dst: dst_id,
322 msg: msg_type.to_string(),
323 missions,
324 store,
325 },
326 );
327 Ok(())
328 }
329 pub fn get_node(&self, node_id: NodeId) -> Option<&Node> {
333 self.0.node_weight(node_id.into())
334 }
335
336 #[allow(dead_code)]
337 pub fn get_node_weight(&self, index: NodeId) -> Option<&Node> {
338 self.0.node_weight(index.into())
339 }
340
341 #[allow(dead_code)]
342 pub fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> {
343 self.0.node_weight_mut(node_id.into())
344 }
345
346 #[allow(dead_code)]
347 pub fn get_edge_weight(&self, index: usize) -> Option<Cnx> {
348 self.0.edge_weight(EdgeIndex::new(index)).cloned()
349 }
350
351 #[allow(dead_code)]
352 pub fn get_node_output_msg_type(&self, node_id: &str) -> Option<String> {
353 self.0.node_indices().find_map(|node_index| {
354 if let Some(node) = self.0.node_weight(node_index) {
355 if node.id != node_id {
356 return None;
357 }
358 let edges: Vec<_> = self
359 .0
360 .edges_directed(node_index, Outgoing)
361 .map(|edge| edge.id().index())
362 .collect();
363 if edges.is_empty() {
364 panic!("A CuSrcTask is configured with no task connected to it.")
365 }
366 let cnx = self
367 .0
368 .edge_weight(EdgeIndex::new(edges[0]))
369 .expect("Found an cnx id but could not retrieve it back");
370 return Some(cnx.msg.clone());
371 }
372 None
373 })
374 }
375
376 #[allow(dead_code)]
377 pub fn get_node_input_msg_type(&self, node_id: &str) -> Option<String> {
378 self.0.node_indices().find_map(|node_index| {
379 if let Some(node) = self.0.node_weight(node_index) {
380 if node.id != node_id {
381 return None;
382 }
383 let edges: Vec<_> = self
384 .0
385 .edges_directed(node_index, Incoming)
386 .map(|edge| edge.id().index())
387 .collect();
388 if edges.is_empty() {
389 return None;
390 }
391 let cnx = self
392 .0
393 .edge_weight(EdgeIndex::new(edges[0]))
394 .expect("Found an cnx id but could not retrieve it back");
395 return Some(cnx.msg.clone());
396 }
397 None
398 })
399 }
400
401 fn get_edges_by_direction(
403 &self,
404 node_id: NodeId,
405 direction: petgraph::Direction,
406 ) -> CuResult<Vec<usize>> {
407 Ok(self
408 .0
409 .edges_directed(node_id.into(), direction)
410 .map(|edge| edge.id().index())
411 .collect())
412 }
413
414 pub fn get_src_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
415 self.get_edges_by_direction(node_id, Outgoing)
416 }
417
418 pub fn get_dst_edges(&self, node_id: NodeId) -> CuResult<Vec<usize>> {
420 self.get_edges_by_direction(node_id, Incoming)
421 }
422
423 #[allow(dead_code)]
426 pub fn connect(&mut self, source: NodeId, target: NodeId, msg_type: &str) -> CuResult<()> {
427 self.connect_ext(source, target, msg_type, None, None)
428 }
429}
430
431impl std::ops::Index<NodeIndex> for CuGraph {
432 type Output = Node;
433
434 fn index(&self, index: NodeIndex) -> &Self::Output {
435 &self.0[index]
436 }
437}
438
439#[derive(Debug, Clone)]
440pub enum ConfigGraphs {
441 Simple(CuGraph),
442 Missions(HashMap<String, CuGraph>),
443}
444
445impl ConfigGraphs {
446 #[allow(dead_code)]
449 pub fn get_all_missions_graphs(&self) -> HashMap<String, CuGraph> {
450 match self {
451 Simple(graph) => {
452 let mut map = HashMap::new();
453 map.insert("default".to_string(), graph.clone());
454 map
455 }
456 Missions(graphs) => graphs.clone(),
457 }
458 }
459
460 #[allow(dead_code)]
461 pub fn get_default_mission_graph(&self) -> CuResult<&CuGraph> {
462 match self {
463 Simple(graph) => Ok(graph),
464 Missions(graphs) => {
465 if graphs.len() == 1 {
466 Ok(graphs.values().next().unwrap())
467 } else {
468 Err("Cannot get default mission graph from mission config".into())
469 }
470 }
471 }
472 }
473
474 #[allow(dead_code)]
475 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
476 match self {
477 Simple(graph) => {
478 if mission_id.is_none() || mission_id.unwrap() == "default" {
479 Ok(graph)
480 } else {
481 Err("Cannot get mission graph from simple config".into())
482 }
483 }
484 Missions(graphs) => {
485 if let Some(id) = mission_id {
486 graphs
487 .get(id)
488 .ok_or_else(|| format!("Mission {id} not found").into())
489 } else {
490 Err("Mission ID required for mission configs".into())
491 }
492 }
493 }
494 }
495
496 #[allow(dead_code)]
497 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
498 match self {
499 Simple(ref mut graph) => {
500 if mission_id.is_none() {
501 Ok(graph)
502 } else {
503 Err("Cannot get mission graph from simple config".into())
504 }
505 }
506 Missions(ref mut graphs) => {
507 if let Some(id) = mission_id {
508 graphs
509 .get_mut(id)
510 .ok_or_else(|| format!("Mission {id} not found").into())
511 } else {
512 Err("Mission ID required for mission configs".into())
513 }
514 }
515 }
516 }
517
518 pub fn add_mission(&mut self, mission_id: &str) -> CuResult<&mut CuGraph> {
519 match self {
520 Simple(_) => Err("Cannot add mission to simple config".into()),
521 Missions(graphs) => {
522 if graphs.contains_key(mission_id) {
523 Err(format!("Mission {mission_id} already exists").into())
524 } else {
525 let graph = CuGraph::default();
526 graphs.insert(mission_id.to_string(), graph);
527 Ok(graphs.get_mut(mission_id).unwrap())
529 }
530 }
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
541pub struct CuConfig {
542 pub monitor: Option<MonitorConfig>,
544 pub logging: Option<LoggingConfig>,
546 pub graphs: ConfigGraphs,
548}
549
550#[derive(Serialize, Deserialize, Default, Debug, Clone)]
551pub struct MonitorConfig {
552 #[serde(rename = "type")]
553 type_: String,
554 #[serde(skip_serializing_if = "Option::is_none")]
555 config: Option<ComponentConfig>,
556}
557
558impl MonitorConfig {
559 #[allow(dead_code)]
560 pub fn get_type(&self) -> &str {
561 &self.type_
562 }
563
564 #[allow(dead_code)]
565 pub fn get_config(&self) -> Option<&ComponentConfig> {
566 self.config.as_ref()
567 }
568}
569
570fn default_as_true() -> bool {
571 true
572}
573
574#[derive(Serialize, Deserialize, Default, Debug, Clone)]
575pub struct LoggingConfig {
576 #[serde(skip_serializing_if = "Option::is_none")]
577 pub slab_size_mib: Option<u64>,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub section_size_mib: Option<u64>,
580 #[serde(default = "default_as_true", skip_serializing_if = "Clone::clone")]
581 pub enable_task_logging: bool,
582}
583
584#[derive(Serialize, Deserialize, Debug, Clone)]
586pub struct MissionsConfig {
587 pub id: String,
588}
589
590#[derive(Serialize, Deserialize, Debug, Clone)]
592pub struct IncludesConfig {
593 pub path: String,
594 pub params: HashMap<String, Value>,
595 pub missions: Option<Vec<String>>,
596}
597
598#[derive(Serialize, Deserialize, Default)]
600struct CuConfigRepresentation {
601 tasks: Option<Vec<Node>>,
602 cnx: Option<Vec<Cnx>>,
603 monitor: Option<MonitorConfig>,
604 logging: Option<LoggingConfig>,
605 missions: Option<Vec<MissionsConfig>>,
606 includes: Option<Vec<IncludesConfig>>,
607}
608
609fn deserialize_config_representation<E>(
611 representation: &CuConfigRepresentation,
612) -> Result<CuConfig, E>
613where
614 E: From<String>,
615{
616 let mut cuconfig = CuConfig::default();
617
618 if let Some(mission_configs) = &representation.missions {
619 let mut missions = Missions(HashMap::new());
621
622 for mission_config in mission_configs {
623 let mission_id = mission_config.id.as_str();
624 let graph = missions
625 .add_mission(mission_id)
626 .map_err(|e| E::from(e.to_string()))?;
627
628 if let Some(tasks) = &representation.tasks {
629 for task in tasks {
630 if let Some(task_missions) = &task.missions {
631 if task_missions.contains(&mission_id.to_owned()) {
633 graph
634 .add_node(task.clone())
635 .map_err(|e| E::from(e.to_string()))?;
636 }
637 } else {
638 graph
640 .add_node(task.clone())
641 .map_err(|e| E::from(e.to_string()))?;
642 }
643 }
644 }
645
646 if let Some(cnx) = &representation.cnx {
647 for c in cnx {
648 if let Some(cnx_missions) = &c.missions {
649 if cnx_missions.contains(&mission_id.to_owned()) {
651 let src = graph
652 .node_indices()
653 .into_iter()
654 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
655 .ok_or_else(|| {
656 E::from(format!("Source node not found: {}", c.src))
657 })?;
658 let dst = graph
659 .node_indices()
660 .into_iter()
661 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
662 .ok_or_else(|| {
663 E::from(format!("Destination node not found: {}", c.dst))
664 })?;
665 graph
666 .connect_ext(
667 src.index() as NodeId,
668 dst.index() as NodeId,
669 &c.msg,
670 c.store,
671 Some(cnx_missions.clone()),
672 )
673 .map_err(|e| E::from(e.to_string()))?;
674 }
675 } else {
676 let src = graph
678 .node_indices()
679 .into_iter()
680 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
681 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
682 let dst = graph
683 .node_indices()
684 .into_iter()
685 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
686 .ok_or_else(|| {
687 E::from(format!("Destination node not found: {}", c.dst))
688 })?;
689 graph
690 .connect_ext(
691 src.index() as NodeId,
692 dst.index() as NodeId,
693 &c.msg,
694 c.store,
695 None,
696 )
697 .map_err(|e| E::from(e.to_string()))?;
698 }
699 }
700 }
701 }
702 cuconfig.graphs = missions;
703 } else {
704 let mut graph = CuGraph::default();
706
707 if let Some(tasks) = &representation.tasks {
708 for task in tasks {
709 graph
710 .add_node(task.clone())
711 .map_err(|e| E::from(e.to_string()))?;
712 }
713 }
714
715 if let Some(cnx) = &representation.cnx {
716 for c in cnx {
717 let src = graph
718 .node_indices()
719 .into_iter()
720 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.src)
721 .ok_or_else(|| E::from(format!("Source node not found: {}", c.src)))?;
722 let dst = graph
723 .node_indices()
724 .into_iter()
725 .find(|i| graph.get_node(i.index() as NodeId).unwrap().id == c.dst)
726 .ok_or_else(|| E::from(format!("Destination node not found: {}", c.dst)))?;
727 graph
728 .connect_ext(
729 src.index() as NodeId,
730 dst.index() as NodeId,
731 &c.msg,
732 c.store,
733 None,
734 )
735 .map_err(|e| E::from(e.to_string()))?;
736 }
737 }
738 cuconfig.graphs = Simple(graph);
739 }
740
741 cuconfig.monitor = representation.monitor.clone();
742 cuconfig.logging = representation.logging.clone();
743
744 Ok(cuconfig)
745}
746
747impl<'de> Deserialize<'de> for CuConfig {
748 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
750 where
751 D: Deserializer<'de>,
752 {
753 let representation =
754 CuConfigRepresentation::deserialize(deserializer).map_err(serde::de::Error::custom)?;
755
756 match deserialize_config_representation::<String>(&representation) {
758 Ok(config) => Ok(config),
759 Err(e) => Err(serde::de::Error::custom(e)),
760 }
761 }
762}
763
764impl Serialize for CuConfig {
765 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
767 where
768 S: Serializer,
769 {
770 match &self.graphs {
771 Simple(graph) => {
772 let tasks: Vec<Node> = graph
773 .0
774 .node_indices()
775 .map(|idx| graph.0[idx].clone())
776 .collect();
777
778 let cnx: Vec<Cnx> = graph
779 .0
780 .edge_indices()
781 .map(|edge| graph.0[edge].clone())
782 .collect();
783
784 CuConfigRepresentation {
785 tasks: Some(tasks),
786 cnx: Some(cnx),
787 monitor: self.monitor.clone(),
788 logging: self.logging.clone(),
789 missions: None,
790 includes: None,
791 }
792 .serialize(serializer)
793 }
794 Missions(graphs) => {
795 let missions = graphs
796 .keys()
797 .map(|id| MissionsConfig { id: id.clone() })
798 .collect();
799
800 let mut tasks = Vec::new();
802 let mut cnx = Vec::new();
803
804 for graph in graphs.values() {
805 for node_idx in graph.node_indices() {
807 let node = &graph[node_idx];
808 if !tasks.iter().any(|n: &Node| n.id == node.id) {
809 tasks.push(node.clone());
810 }
811 }
812
813 for edge_idx in graph.0.edge_indices() {
815 let edge = &graph.0[edge_idx];
816 if !cnx.iter().any(|c: &Cnx| {
817 c.src == edge.src && c.dst == edge.dst && c.msg == edge.msg
818 }) {
819 cnx.push(edge.clone());
820 }
821 }
822 }
823
824 CuConfigRepresentation {
825 tasks: Some(tasks),
826 cnx: Some(cnx),
827 monitor: self.monitor.clone(),
828 logging: self.logging.clone(),
829 missions: Some(missions),
830 includes: None,
831 }
832 .serialize(serializer)
833 }
834 }
835 }
836}
837
838impl Default for CuConfig {
839 fn default() -> Self {
840 CuConfig {
841 graphs: Simple(CuGraph(StableDiGraph::new())),
842 monitor: None,
843 logging: None,
844 }
845 }
846}
847
848impl CuConfig {
851 #[allow(dead_code)]
852 pub fn new_simple_type() -> Self {
853 Self::default()
854 }
855
856 #[allow(dead_code)]
857 pub fn new_mission_type() -> Self {
858 CuConfig {
859 graphs: Missions(HashMap::new()),
860 monitor: None,
861 logging: None,
862 }
863 }
864
865 fn get_options() -> Options {
866 Options::default()
867 .with_default_extension(Extensions::IMPLICIT_SOME)
868 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
869 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
870 }
871
872 #[allow(dead_code)]
873 pub fn serialize_ron(&self) -> String {
874 let ron = Self::get_options();
875 let pretty = ron::ser::PrettyConfig::default();
876 ron.to_string_pretty(&self, pretty).unwrap()
877 }
878
879 #[allow(dead_code)]
880 pub fn deserialize_ron(ron: &str) -> Self {
881 match Self::get_options().from_str(ron) {
882 Ok(representation) => Self::deserialize_impl(representation).unwrap_or_else(|e| {
883 panic!("Error deserializing configuration: {e}");
884 }),
885 Err(e) => panic!(
886 "Syntax Error in config: {} at position {}",
887 e.code, e.position
888 ),
889 }
890 }
891
892 fn deserialize_impl(representation: CuConfigRepresentation) -> Result<Self, String> {
893 deserialize_config_representation(&representation)
894 }
895
896 pub fn render(
898 &self,
899 output: &mut dyn std::io::Write,
900 mission_id: Option<&str>,
901 ) -> CuResult<()> {
902 writeln!(output, "digraph G {{").unwrap();
903
904 let graph = self.get_graph(mission_id)?;
905
906 for index in graph.node_indices() {
907 let node = &graph[index];
908 let config_str = match &node.config {
909 Some(config) => {
910 let config_str = config
911 .0
912 .iter()
913 .map(|(k, v)| format!("<B>{k}</B> = {v}<BR ALIGN=\"LEFT\"/>"))
914 .collect::<Vec<String>>()
915 .join("\n");
916 format!("____________<BR/><BR ALIGN=\"LEFT\"/>{config_str}")
917 }
918 None => String::new(),
919 };
920 writeln!(output, "{} [", index.index()).unwrap();
921 writeln!(output, "shape=box,").unwrap();
922 writeln!(output, "style=\"rounded, filled\",").unwrap();
923 writeln!(output, "fontname=\"Noto Sans\"").unwrap();
924
925 let is_src = graph
926 .get_dst_edges(index.index() as NodeId)
927 .unwrap_or_default()
928 .is_empty();
929 let is_sink = graph
930 .get_src_edges(index.index() as NodeId)
931 .unwrap_or_default()
932 .is_empty();
933 if is_src {
934 writeln!(output, "fillcolor=lightgreen,").unwrap();
935 } else if is_sink {
936 writeln!(output, "fillcolor=lightblue,").unwrap();
937 } else {
938 writeln!(output, "fillcolor=lightgrey,").unwrap();
939 }
940 writeln!(output, "color=grey,").unwrap();
941
942 writeln!(output, "labeljust=l,").unwrap();
943 writeln!(
944 output,
945 "label=< <FONT COLOR=\"red\"><B>{}</B></FONT> <FONT COLOR=\"dimgray\">[{}]</FONT><BR ALIGN=\"LEFT\"/>{} >",
946 node.id,
947 node.get_type(),
948 config_str
949 )
950 .unwrap();
951
952 writeln!(output, "];").unwrap();
953 }
954 for edge in graph.0.edge_indices() {
955 let (src, dst) = graph.0.edge_endpoints(edge).unwrap();
956
957 let cnx = &graph.0[edge];
958 let msg = encode_text(&cnx.msg);
959 writeln!(
960 output,
961 "{} -> {} [label=< <B><FONT COLOR=\"gray\">{}</FONT></B> >];",
962 src.index(),
963 dst.index(),
964 msg
965 )
966 .unwrap();
967 }
968 writeln!(output, "}}").unwrap();
969 Ok(())
970 }
971
972 #[allow(dead_code)]
973 pub fn get_all_instances_configs(
974 &self,
975 mission_id: Option<&str>,
976 ) -> Vec<Option<&ComponentConfig>> {
977 let graph = self.graphs.get_graph(mission_id).unwrap();
978 graph
979 .get_all_nodes()
980 .iter()
981 .map(|(_, node)| node.get_instance_config())
982 .collect()
983 }
984
985 #[allow(dead_code)]
986 pub fn get_graph(&self, mission_id: Option<&str>) -> CuResult<&CuGraph> {
987 self.graphs.get_graph(mission_id)
988 }
989
990 #[allow(dead_code)]
991 pub fn get_graph_mut(&mut self, mission_id: Option<&str>) -> CuResult<&mut CuGraph> {
992 self.graphs.get_graph_mut(mission_id)
993 }
994
995 #[allow(dead_code)]
996 pub fn get_monitor_config(&self) -> Option<&MonitorConfig> {
997 self.monitor.as_ref()
998 }
999
1000 pub fn validate_logging_config(&self) -> CuResult<()> {
1003 if let Some(logging) = &self.logging {
1004 return logging.validate();
1005 }
1006 Ok(())
1007 }
1008}
1009
1010impl LoggingConfig {
1011 pub fn validate(&self) -> CuResult<()> {
1013 if let Some(section_size_mib) = self.section_size_mib {
1014 if let Some(slab_size_mib) = self.slab_size_mib {
1015 if section_size_mib > slab_size_mib {
1016 return Err(CuError::from(format!("Section size ({section_size_mib} MiB) cannot be larger than slab size ({slab_size_mib} MiB). Adjust the parameters accordingly.")));
1017 }
1018 }
1019 }
1020
1021 Ok(())
1022 }
1023}
1024
1025fn substitute_parameters(content: &str, params: &HashMap<String, Value>) -> String {
1026 let mut result = content.to_string();
1027
1028 for (key, value) in params {
1029 let pattern = format!("{{{{{key}}}}}");
1030 result = result.replace(&pattern, &value.to_string());
1031 }
1032
1033 result
1034}
1035
1036fn process_includes(
1038 file_path: &str,
1039 base_representation: CuConfigRepresentation,
1040 processed_files: &mut Vec<String>,
1041) -> CuResult<CuConfigRepresentation> {
1042 processed_files.push(file_path.to_string());
1044
1045 let mut result = base_representation;
1046
1047 if let Some(includes) = result.includes.take() {
1048 for include in includes {
1049 let include_path = if include.path.starts_with('/') {
1050 include.path.clone()
1051 } else {
1052 let current_dir = std::path::Path::new(file_path)
1053 .parent()
1054 .unwrap_or_else(|| std::path::Path::new(""))
1055 .to_string_lossy()
1056 .to_string();
1057
1058 format!("{}/{}", current_dir, include.path)
1059 };
1060
1061 let include_content = read_to_string(&include_path).map_err(|e| {
1062 CuError::from(format!("Failed to read include file: {include_path}"))
1063 .add_cause(e.to_string().as_str())
1064 })?;
1065
1066 let processed_content = substitute_parameters(&include_content, &include.params);
1067
1068 let mut included_representation: CuConfigRepresentation = match Options::default()
1069 .with_default_extension(Extensions::IMPLICIT_SOME)
1070 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1071 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1072 .from_str(&processed_content)
1073 {
1074 Ok(rep) => rep,
1075 Err(e) => {
1076 return Err(CuError::from(format!(
1077 "Failed to parse include file: {} - Error: {} at position {}",
1078 include_path, e.code, e.position
1079 )));
1080 }
1081 };
1082
1083 included_representation =
1084 process_includes(&include_path, included_representation, processed_files)?;
1085
1086 if let Some(included_tasks) = included_representation.tasks {
1087 if result.tasks.is_none() {
1088 result.tasks = Some(included_tasks);
1089 } else {
1090 let mut tasks = result.tasks.take().unwrap();
1091 for included_task in included_tasks {
1092 if !tasks.iter().any(|t| t.id == included_task.id) {
1093 tasks.push(included_task);
1094 }
1095 }
1096 result.tasks = Some(tasks);
1097 }
1098 }
1099
1100 if let Some(included_cnx) = included_representation.cnx {
1101 if result.cnx.is_none() {
1102 result.cnx = Some(included_cnx);
1103 } else {
1104 let mut cnx = result.cnx.take().unwrap();
1105 for included_c in included_cnx {
1106 if !cnx
1107 .iter()
1108 .any(|c| c.src == included_c.src && c.dst == included_c.dst)
1109 {
1110 cnx.push(included_c);
1111 }
1112 }
1113 result.cnx = Some(cnx);
1114 }
1115 }
1116
1117 if result.monitor.is_none() {
1118 result.monitor = included_representation.monitor;
1119 }
1120
1121 if result.logging.is_none() {
1122 result.logging = included_representation.logging;
1123 }
1124
1125 if let Some(included_missions) = included_representation.missions {
1126 if result.missions.is_none() {
1127 result.missions = Some(included_missions);
1128 } else {
1129 let mut missions = result.missions.take().unwrap();
1130 for included_mission in included_missions {
1131 if !missions.iter().any(|m| m.id == included_mission.id) {
1132 missions.push(included_mission);
1133 }
1134 }
1135 result.missions = Some(missions);
1136 }
1137 }
1138 }
1139 }
1140
1141 Ok(result)
1142}
1143
1144pub fn read_configuration(config_filename: &str) -> CuResult<CuConfig> {
1146 let config_content = read_to_string(config_filename).map_err(|e| {
1147 CuError::from(format!(
1148 "Failed to read configuration file: {:?}",
1149 &config_filename
1150 ))
1151 .add_cause(e.to_string().as_str())
1152 })?;
1153 read_configuration_str(config_content, Some(config_filename))
1154}
1155
1156fn parse_config_string(content: &str) -> CuResult<CuConfigRepresentation> {
1160 Options::default()
1161 .with_default_extension(Extensions::IMPLICIT_SOME)
1162 .with_default_extension(Extensions::UNWRAP_NEWTYPES)
1163 .with_default_extension(Extensions::UNWRAP_VARIANT_NEWTYPES)
1164 .from_str(content)
1165 .map_err(|e| {
1166 CuError::from(format!(
1167 "Failed to parse configuration: Error: {} at position {}",
1168 e.code, e.position
1169 ))
1170 })
1171}
1172
1173fn config_representation_to_config(representation: CuConfigRepresentation) -> CuResult<CuConfig> {
1176 let cuconfig = CuConfig::deserialize_impl(representation)
1177 .map_err(|e| CuError::from(format!("Error deserializing configuration: {e}")))?;
1178
1179 cuconfig.validate_logging_config()?;
1180
1181 Ok(cuconfig)
1182}
1183
1184pub fn read_configuration_str(
1185 config_content: String,
1186 file_path: Option<&str>,
1187) -> CuResult<CuConfig> {
1188 let representation = parse_config_string(&config_content)?;
1190
1191 let processed_representation = if let Some(path) = file_path {
1193 process_includes(path, representation, &mut Vec::new())?
1194 } else {
1195 representation
1196 };
1197
1198 config_representation_to_config(processed_representation)
1200}
1201
1202#[cfg(test)]
1204mod tests {
1205 use super::*;
1206
1207 #[test]
1208 fn test_plain_serialize() {
1209 let mut config = CuConfig::default();
1210 let graph = config.get_graph_mut(None).unwrap();
1211 let n1 = graph
1212 .add_node(Node::new("test1", "package::Plugin1"))
1213 .unwrap();
1214 let n2 = graph
1215 .add_node(Node::new("test2", "package::Plugin2"))
1216 .unwrap();
1217 graph.connect(n1, n2, "msgpkg::MsgType").unwrap();
1218 let serialized = config.serialize_ron();
1219 let deserialized = CuConfig::deserialize_ron(&serialized);
1220 let graph = config.graphs.get_graph(None).unwrap();
1221 let deserialized_graph = deserialized.graphs.get_graph(None).unwrap();
1222 assert_eq!(graph.0.node_count(), deserialized_graph.0.node_count());
1223 assert_eq!(graph.0.edge_count(), deserialized_graph.0.edge_count());
1224 }
1225
1226 #[test]
1227 fn test_serialize_with_params() {
1228 let mut config = CuConfig::default();
1229 let graph = config.get_graph_mut(None).unwrap();
1230 let mut camera = Node::new("copper-camera", "camerapkg::Camera");
1231 camera.set_param::<Value>("resolution-height", 1080.into());
1232 graph.add_node(camera).unwrap();
1233 let serialized = config.serialize_ron();
1234 let config = CuConfig::deserialize_ron(&serialized);
1235 let deserialized = config.get_graph(None).unwrap();
1236 assert_eq!(
1237 deserialized
1238 .get_node(0)
1239 .unwrap()
1240 .get_param::<i32>("resolution-height")
1241 .unwrap(),
1242 1080
1243 );
1244 }
1245
1246 #[test]
1247 #[should_panic(expected = "Syntax Error in config: Expected opening `[` at position 1:10")]
1248 fn test_deserialization_error() {
1249 let txt = r#"( tasks: (), cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1251 CuConfig::deserialize_ron(txt);
1252 }
1253 #[test]
1254 fn test_missions() {
1255 let txt = r#"( missions: [ (id: "data_collection"), (id: "autonomous")])"#;
1256 let config = CuConfig::deserialize_ron(txt);
1257 let graph = config.graphs.get_graph(Some("data_collection")).unwrap();
1258 assert!(graph.0.node_count() == 0);
1259 let graph = config.graphs.get_graph(Some("autonomous")).unwrap();
1260 assert!(graph.0.node_count() == 0);
1261 }
1262
1263 #[test]
1264 fn test_monitor() {
1265 let txt = r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", ) ) "#;
1266 let config = CuConfig::deserialize_ron(txt);
1267 assert_eq!(config.monitor.as_ref().unwrap().type_, "ExampleMonitor");
1268
1269 let txt =
1270 r#"( tasks: [], cnx: [], monitor: (type: "ExampleMonitor", config: { "toto": 4, } )) "#;
1271 let config = CuConfig::deserialize_ron(txt);
1272 assert_eq!(
1273 config.monitor.as_ref().unwrap().config.as_ref().unwrap().0["toto"].0,
1274 4u8.into()
1275 );
1276 }
1277
1278 #[test]
1279 fn test_logging_parameters() {
1280 let txt = r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, enable_task_logging: false ),) "#;
1282
1283 let config = CuConfig::deserialize_ron(txt);
1284 assert!(config.logging.is_some());
1285 let logging_config = config.logging.unwrap();
1286 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1287 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1288 assert!(!logging_config.enable_task_logging);
1289
1290 let txt =
1292 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100, ),) "#;
1293 let config = CuConfig::deserialize_ron(txt);
1294 assert!(config.logging.is_some());
1295 let logging_config = config.logging.unwrap();
1296 assert_eq!(logging_config.slab_size_mib.unwrap(), 1024);
1297 assert_eq!(logging_config.section_size_mib.unwrap(), 100);
1298 assert!(logging_config.enable_task_logging);
1299 }
1300
1301 #[test]
1302 fn test_validate_logging_config() {
1303 let txt =
1305 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 1024, section_size_mib: 100 ) )"#;
1306 let config = CuConfig::deserialize_ron(txt);
1307 assert!(config.validate_logging_config().is_ok());
1308
1309 let txt =
1311 r#"( tasks: [], cnx: [], logging: ( slab_size_mib: 100, section_size_mib: 1024 ) )"#;
1312 let config = CuConfig::deserialize_ron(txt);
1313 assert!(config.validate_logging_config().is_err());
1314 }
1315
1316 #[test]
1318 fn test_deserialization_edge_id_assignment() {
1319 let txt = r#"(
1322 tasks: [(id: "src1", type: "a"), (id: "src2", type: "b"), (id: "sink", type: "c")],
1323 cnx: [(src: "src2", dst: "sink", msg: "msg1"), (src: "src1", dst: "sink", msg: "msg2")]
1324 )"#;
1325 let config = CuConfig::deserialize_ron(txt);
1326 let graph = config.graphs.get_graph(None).unwrap();
1327 assert!(config.validate_logging_config().is_ok());
1328
1329 let src1_id = 0;
1331 assert_eq!(graph.get_node(src1_id).unwrap().id, "src1");
1332 let src2_id = 1;
1333 assert_eq!(graph.get_node(src2_id).unwrap().id, "src2");
1334
1335 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
1338 assert_eq!(src1_edge_id, 1);
1339 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
1340 assert_eq!(src2_edge_id, 0);
1341 }
1342
1343 #[test]
1344 fn test_simple_missions() {
1345 let txt = r#"(
1347 missions: [ (id: "m1"),
1348 (id: "m2"),
1349 ],
1350 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1351 (id: "src2", type: "b", missions: ["m2"]),
1352 (id: "sink", type: "c")],
1353
1354 cnx: [
1355 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1356 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1357 ],
1358 )
1359 "#;
1360
1361 let config = CuConfig::deserialize_ron(txt);
1362 let m1_graph = config.graphs.get_graph(Some("m1")).unwrap();
1363 assert_eq!(m1_graph.0.edge_count(), 1);
1364 assert_eq!(m1_graph.0.node_count(), 2);
1365 let index = EdgeIndex::new(0);
1366 let cnx = m1_graph.0.edge_weight(index).unwrap();
1367
1368 assert_eq!(cnx.src, "src1");
1369 assert_eq!(cnx.dst, "sink");
1370 assert_eq!(cnx.msg, "u32");
1371 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1372
1373 let m2_graph = config.graphs.get_graph(Some("m2")).unwrap();
1374 assert_eq!(m2_graph.0.edge_count(), 1);
1375 assert_eq!(m2_graph.0.node_count(), 2);
1376 let index = EdgeIndex::new(0);
1377 let cnx = m2_graph.0.edge_weight(index).unwrap();
1378 assert_eq!(cnx.src, "src2");
1379 assert_eq!(cnx.dst, "sink");
1380 assert_eq!(cnx.msg, "u32");
1381 assert_eq!(cnx.missions, Some(vec!["m2".to_string()]));
1382 }
1383 #[test]
1384 fn test_mission_serde() {
1385 let txt = r#"(
1387 missions: [ (id: "m1"),
1388 (id: "m2"),
1389 ],
1390 tasks: [(id: "src1", type: "a", missions: ["m1"]),
1391 (id: "src2", type: "b", missions: ["m2"]),
1392 (id: "sink", type: "c")],
1393
1394 cnx: [
1395 (src: "src1", dst: "sink", msg: "u32", missions: ["m1"]),
1396 (src: "src2", dst: "sink", msg: "u32", missions: ["m2"]),
1397 ],
1398 )
1399 "#;
1400
1401 let config = CuConfig::deserialize_ron(txt);
1402 let serialized = config.serialize_ron();
1403 let deserialized = CuConfig::deserialize_ron(&serialized);
1404 let m1_graph = deserialized.graphs.get_graph(Some("m1")).unwrap();
1405 assert_eq!(m1_graph.0.edge_count(), 1);
1406 assert_eq!(m1_graph.0.node_count(), 2);
1407 let index = EdgeIndex::new(0);
1408 let cnx = m1_graph.0.edge_weight(index).unwrap();
1409 assert_eq!(cnx.src, "src1");
1410 assert_eq!(cnx.dst, "sink");
1411 assert_eq!(cnx.msg, "u32");
1412 assert_eq!(cnx.missions, Some(vec!["m1".to_string()]));
1413 }
1414}