Skip to main content

dendryform_core/
connector.rs

1//! Visual connector types between tiers.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// The visual style of a connector between tiers.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10#[non_exhaustive]
11pub enum ConnectorStyle {
12    /// Solid vertical line with a downward arrowhead.
13    Line,
14    /// A row of small dots.
15    Dots,
16}
17
18impl fmt::Display for ConnectorStyle {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Line => f.write_str("line"),
22            Self::Dots => f.write_str("dots"),
23        }
24    }
25}
26
27/// A visual connector between adjacent tiers.
28///
29/// Connectors provide visual flow cues but do not carry semantic
30/// relationship data (use [`Edge`](crate::Edge) for that).
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub struct Connector {
34    style: ConnectorStyle,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    label: Option<String>,
37}
38
39impl Connector {
40    /// Creates a new connector with the given style.
41    pub fn new(style: ConnectorStyle) -> Self {
42        Self { style, label: None }
43    }
44
45    /// Creates a new connector with a label (line connectors only).
46    pub fn with_label(style: ConnectorStyle, label: &str) -> Self {
47        Self {
48            style,
49            label: Some(label.to_owned()),
50        }
51    }
52
53    /// Returns the visual style.
54    pub fn style(&self) -> ConnectorStyle {
55        self.style
56    }
57
58    /// Returns the optional label.
59    pub fn label(&self) -> Option<&str> {
60        self.label.as_deref()
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_new() {
70        let conn = Connector::new(ConnectorStyle::Dots);
71        assert_eq!(conn.style(), ConnectorStyle::Dots);
72        assert_eq!(conn.label(), None);
73    }
74
75    #[test]
76    fn test_with_label() {
77        let conn = Connector::with_label(ConnectorStyle::Line, "HTTPS");
78        assert_eq!(conn.style(), ConnectorStyle::Line);
79        assert_eq!(conn.label(), Some("HTTPS"));
80    }
81
82    #[test]
83    fn test_serde_round_trip() {
84        let conn = Connector::with_label(ConnectorStyle::Line, "gRPC");
85        let json = serde_json::to_string(&conn).unwrap();
86        let deserialized: Connector = serde_json::from_str(&json).unwrap();
87        assert_eq!(conn, deserialized);
88    }
89
90    #[test]
91    fn test_serde_round_trip_no_label() {
92        let conn = Connector::new(ConnectorStyle::Dots);
93        let json = serde_json::to_string(&conn).unwrap();
94        // label should be omitted via skip_serializing_if
95        assert!(!json.contains("label"));
96        let deserialized: Connector = serde_json::from_str(&json).unwrap();
97        assert_eq!(conn, deserialized);
98    }
99
100    #[test]
101    fn test_connector_style_display() {
102        assert_eq!(format!("{}", ConnectorStyle::Line), "line");
103        assert_eq!(format!("{}", ConnectorStyle::Dots), "dots");
104    }
105
106    #[test]
107    fn test_connector_style_serde() {
108        let line = ConnectorStyle::Line;
109        let json = serde_json::to_string(&line).unwrap();
110        assert_eq!(json, "\"line\"");
111        let deserialized: ConnectorStyle = serde_json::from_str(&json).unwrap();
112        assert_eq!(line, deserialized);
113
114        let dots = ConnectorStyle::Dots;
115        let json = serde_json::to_string(&dots).unwrap();
116        assert_eq!(json, "\"dots\"");
117        let deserialized: ConnectorStyle = serde_json::from_str(&json).unwrap();
118        assert_eq!(dots, deserialized);
119    }
120
121    #[test]
122    fn test_connector_debug() {
123        let conn = Connector::with_label(ConnectorStyle::Line, "HTTPS");
124        let debug = format!("{conn:?}");
125        assert!(debug.contains("Line"));
126        assert!(debug.contains("HTTPS"));
127    }
128
129    #[test]
130    fn test_connector_style_hash() {
131        use std::collections::HashSet;
132        let mut set = HashSet::new();
133        set.insert(ConnectorStyle::Line);
134        set.insert(ConnectorStyle::Dots);
135        set.insert(ConnectorStyle::Line);
136        assert_eq!(set.len(), 2);
137    }
138
139    #[test]
140    fn test_connector_clone_eq() {
141        let a = Connector::with_label(ConnectorStyle::Dots, "test");
142        let b = a.clone();
143        assert_eq!(a, b);
144    }
145}