dendryform-core 0.1.0

Core schema types, validation, theme, and layout plan for dendryform
Documentation
//! Visual connector types between tiers.

use std::fmt;

use serde::{Deserialize, Serialize};

/// The visual style of a connector between tiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ConnectorStyle {
    /// Solid vertical line with a downward arrowhead.
    Line,
    /// A row of small dots.
    Dots,
}

impl fmt::Display for ConnectorStyle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Line => f.write_str("line"),
            Self::Dots => f.write_str("dots"),
        }
    }
}

/// A visual connector between adjacent tiers.
///
/// Connectors provide visual flow cues but do not carry semantic
/// relationship data (use [`Edge`](crate::Edge) for that).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Connector {
    style: ConnectorStyle,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    label: Option<String>,
}

impl Connector {
    /// Creates a new connector with the given style.
    pub fn new(style: ConnectorStyle) -> Self {
        Self { style, label: None }
    }

    /// Creates a new connector with a label (line connectors only).
    pub fn with_label(style: ConnectorStyle, label: &str) -> Self {
        Self {
            style,
            label: Some(label.to_owned()),
        }
    }

    /// Returns the visual style.
    pub fn style(&self) -> ConnectorStyle {
        self.style
    }

    /// Returns the optional label.
    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let conn = Connector::new(ConnectorStyle::Dots);
        assert_eq!(conn.style(), ConnectorStyle::Dots);
        assert_eq!(conn.label(), None);
    }

    #[test]
    fn test_with_label() {
        let conn = Connector::with_label(ConnectorStyle::Line, "HTTPS");
        assert_eq!(conn.style(), ConnectorStyle::Line);
        assert_eq!(conn.label(), Some("HTTPS"));
    }

    #[test]
    fn test_serde_round_trip() {
        let conn = Connector::with_label(ConnectorStyle::Line, "gRPC");
        let json = serde_json::to_string(&conn).unwrap();
        let deserialized: Connector = serde_json::from_str(&json).unwrap();
        assert_eq!(conn, deserialized);
    }

    #[test]
    fn test_serde_round_trip_no_label() {
        let conn = Connector::new(ConnectorStyle::Dots);
        let json = serde_json::to_string(&conn).unwrap();
        // label should be omitted via skip_serializing_if
        assert!(!json.contains("label"));
        let deserialized: Connector = serde_json::from_str(&json).unwrap();
        assert_eq!(conn, deserialized);
    }

    #[test]
    fn test_connector_style_display() {
        assert_eq!(format!("{}", ConnectorStyle::Line), "line");
        assert_eq!(format!("{}", ConnectorStyle::Dots), "dots");
    }

    #[test]
    fn test_connector_style_serde() {
        let line = ConnectorStyle::Line;
        let json = serde_json::to_string(&line).unwrap();
        assert_eq!(json, "\"line\"");
        let deserialized: ConnectorStyle = serde_json::from_str(&json).unwrap();
        assert_eq!(line, deserialized);

        let dots = ConnectorStyle::Dots;
        let json = serde_json::to_string(&dots).unwrap();
        assert_eq!(json, "\"dots\"");
        let deserialized: ConnectorStyle = serde_json::from_str(&json).unwrap();
        assert_eq!(dots, deserialized);
    }

    #[test]
    fn test_connector_debug() {
        let conn = Connector::with_label(ConnectorStyle::Line, "HTTPS");
        let debug = format!("{conn:?}");
        assert!(debug.contains("Line"));
        assert!(debug.contains("HTTPS"));
    }

    #[test]
    fn test_connector_style_hash() {
        use std::collections::HashSet;
        let mut set = HashSet::new();
        set.insert(ConnectorStyle::Line);
        set.insert(ConnectorStyle::Dots);
        set.insert(ConnectorStyle::Line);
        assert_eq!(set.len(), 2);
    }

    #[test]
    fn test_connector_clone_eq() {
        let a = Connector::with_label(ConnectorStyle::Dots, "test");
        let b = a.clone();
        assert_eq!(a, b);
    }
}