Skip to main content

dendryform_core/
tier.rs

1//! Tier type — a horizontal band of nodes.
2
3use serde::{Deserialize, Serialize};
4
5use crate::container::Container;
6use crate::id::NodeId;
7use crate::layout::TierLayout;
8use crate::node::Node;
9
10/// A horizontal band of nodes in the diagram, optionally wrapped in a container.
11///
12/// Each tier has a unique ID, an optional label, a layout hint, and
13/// either a list of nodes or a container (or both).
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub struct Tier {
17    id: NodeId,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    label: Option<String>,
20    #[serde(default, skip_serializing_if = "is_auto")]
21    layout: TierLayout,
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    nodes: Vec<Node>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    container: Option<Container>,
26}
27
28fn is_auto(layout: &TierLayout) -> bool {
29    *layout == TierLayout::Auto
30}
31
32impl Tier {
33    /// Creates a new tier with nodes.
34    pub fn new(id: NodeId, nodes: Vec<Node>) -> Self {
35        Self {
36            id,
37            label: None,
38            layout: TierLayout::default(),
39            nodes,
40            container: None,
41        }
42    }
43
44    /// Creates a new tier with a container.
45    pub fn with_container(id: NodeId, container: Container) -> Self {
46        Self {
47            id,
48            label: None,
49            layout: TierLayout::default(),
50            nodes: Vec::new(),
51            container: Some(container),
52        }
53    }
54
55    /// Returns the tier's unique identifier.
56    pub fn id(&self) -> &NodeId {
57        &self.id
58    }
59
60    /// Returns the optional tier heading label.
61    pub fn label(&self) -> Option<&str> {
62        self.label.as_deref()
63    }
64
65    /// Sets the tier heading label.
66    pub fn set_label(&mut self, label: &str) {
67        self.label = Some(label.to_owned());
68    }
69
70    /// Returns the layout configuration.
71    pub fn layout(&self) -> &TierLayout {
72        &self.layout
73    }
74
75    /// Sets the layout configuration.
76    pub fn set_layout(&mut self, layout: TierLayout) {
77        self.layout = layout;
78    }
79
80    /// Returns the nodes in this tier.
81    pub fn nodes(&self) -> &[Node] {
82        &self.nodes
83    }
84
85    /// Returns the optional container.
86    pub fn container(&self) -> Option<&Container> {
87        self.container.as_ref()
88    }
89
90    /// Returns `true` if this tier has neither nodes nor a container.
91    pub fn is_empty(&self) -> bool {
92        self.nodes.is_empty() && self.container.is_none()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::color::Color;
100    use crate::kind::NodeKind;
101    use crate::layer::Layer;
102
103    #[test]
104    fn test_new_with_nodes() {
105        let node = Node::builder()
106            .id(NodeId::new("app").unwrap())
107            .kind(NodeKind::System)
108            .color(Color::Blue)
109            .icon("◇")
110            .title("App")
111            .description("The app")
112            .build()
113            .unwrap();
114
115        let tier = Tier::new(NodeId::new("clients").unwrap(), vec![node]);
116        assert_eq!(tier.id().as_str(), "clients");
117        assert_eq!(tier.nodes().len(), 1);
118        assert!(!tier.is_empty());
119    }
120
121    #[test]
122    fn test_empty_tier() {
123        let tier = Tier::new(NodeId::new("empty").unwrap(), vec![]);
124        assert!(tier.is_empty());
125    }
126
127    #[test]
128    fn test_with_container() {
129        use crate::container::{Container, ContainerBorder};
130
131        let container = Container::new(
132            "server",
133            ContainerBorder::Solid,
134            Color::Green,
135            vec![Layer::Tier(Tier::new(
136                NodeId::new("inner").unwrap(),
137                vec![
138                    Node::builder()
139                        .id(NodeId::new("api").unwrap())
140                        .kind(NodeKind::System)
141                        .color(Color::Green)
142                        .icon("x")
143                        .title("API")
144                        .description("desc")
145                        .build()
146                        .unwrap(),
147                ],
148            ))],
149        );
150        let tier = Tier::with_container(NodeId::new("server").unwrap(), container);
151        assert!(!tier.is_empty());
152        assert!(tier.container().is_some());
153        assert!(tier.nodes().is_empty());
154    }
155
156    #[test]
157    fn test_label_and_set_label() {
158        let mut tier = Tier::new(NodeId::new("t").unwrap(), vec![]);
159        assert!(tier.label().is_none());
160        tier.set_label("My Tier");
161        assert_eq!(tier.label(), Some("My Tier"));
162    }
163
164    #[test]
165    fn test_layout_and_set_layout() {
166        use crate::layout::TierLayout;
167
168        let mut tier = Tier::new(NodeId::new("t").unwrap(), vec![]);
169        assert_eq!(*tier.layout(), TierLayout::Auto);
170        tier.set_layout(TierLayout::Single);
171        assert_eq!(*tier.layout(), TierLayout::Single);
172        tier.set_layout(TierLayout::Grid { columns: 3 });
173        assert_eq!(*tier.layout(), TierLayout::Grid { columns: 3 });
174    }
175
176    #[test]
177    fn test_tier_serde_round_trip() {
178        let node = Node::builder()
179            .id(NodeId::new("app").unwrap())
180            .kind(NodeKind::System)
181            .color(Color::Blue)
182            .icon("x")
183            .title("App")
184            .description("desc")
185            .build()
186            .unwrap();
187        let tier = Tier::new(NodeId::new("main").unwrap(), vec![node]);
188        let json = serde_json::to_string(&tier).unwrap();
189        let deserialized: Tier = serde_json::from_str(&json).unwrap();
190        assert_eq!(tier, deserialized);
191    }
192}