Skip to main content

dendryform_core/
layer.rs

1//! Layer enum — the ordered visual elements of a diagram.
2
3use serde::de::{self, MapAccess, Visitor};
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::connector::Connector;
7use crate::tier::Tier;
8
9/// Directional labels between tiers (typically above external services).
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub struct FlowLabels {
13    items: Vec<String>,
14}
15
16impl FlowLabels {
17    /// Creates new flow labels.
18    pub fn new(items: Vec<String>) -> Self {
19        Self { items }
20    }
21
22    /// Returns the label items.
23    pub fn items(&self) -> &[String] {
24        &self.items
25    }
26}
27
28/// A single visual element in the diagram's layer stack.
29///
30/// Layers are rendered in order from top to bottom. Each layer is
31/// exactly one of: a tier (horizontal band of nodes), a connector
32/// (visual link between tiers), or flow labels (directional arrows).
33///
34/// In YAML, each layer is a map with a single key identifying the variant:
35/// ```yaml
36/// - tier:
37///     id: main
38///     nodes: [...]
39/// - connector:
40///     style: line
41/// - flow_labels:
42///     items: [...]
43/// ```
44#[derive(Debug, Clone, PartialEq, Serialize)]
45#[serde(rename_all = "snake_case")]
46#[non_exhaustive]
47pub enum Layer {
48    /// A horizontal band of nodes.
49    Tier(Tier),
50    /// A visual connector between adjacent tiers.
51    Connector(Connector),
52    /// Directional labels between tiers.
53    FlowLabels(FlowLabels),
54}
55
56impl<'de> Deserialize<'de> for Layer {
57    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58    where
59        D: Deserializer<'de>,
60    {
61        deserializer.deserialize_map(LayerVisitor)
62    }
63}
64
65struct LayerVisitor;
66
67impl<'de> Visitor<'de> for LayerVisitor {
68    type Value = Layer;
69
70    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.write_str("a map with a single key: \"tier\", \"connector\", or \"flow_labels\"")
72    }
73
74    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
75    where
76        A: MapAccess<'de>,
77    {
78        let key: String = map
79            .next_key()?
80            .ok_or_else(|| de::Error::custom("expected a layer variant key"))?;
81
82        let layer = match key.as_str() {
83            "tier" => Layer::Tier(map.next_value()?),
84            "connector" => Layer::Connector(map.next_value()?),
85            "flow_labels" => Layer::FlowLabels(map.next_value()?),
86            other => {
87                return Err(de::Error::unknown_variant(
88                    other,
89                    &["tier", "connector", "flow_labels"],
90                ));
91            }
92        };
93
94        Ok(layer)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::connector::ConnectorStyle;
102    use crate::id::NodeId;
103
104    #[test]
105    fn test_flow_labels() {
106        let labels = FlowLabels::new(vec!["SQL queries".to_owned(), "cache reads".to_owned()]);
107        assert_eq!(labels.items().len(), 2);
108    }
109
110    #[test]
111    fn test_layer_variants() {
112        let tier = Tier::new(NodeId::new("test").unwrap(), vec![]);
113        let layer = Layer::Tier(tier);
114        assert!(matches!(layer, Layer::Tier(_)));
115
116        let conn = Connector::new(ConnectorStyle::Dots);
117        let layer = Layer::Connector(conn);
118        assert!(matches!(layer, Layer::Connector(_)));
119
120        let labels = FlowLabels::new(vec!["test".to_owned()]);
121        let layer = Layer::FlowLabels(labels);
122        assert!(matches!(layer, Layer::FlowLabels(_)));
123    }
124
125    #[test]
126    fn test_yaml_round_trip_tier() {
127        let tier = Tier::new(NodeId::new("test").unwrap(), vec![]);
128        let layer = Layer::Tier(tier);
129        let json = serde_json::to_string(&layer).unwrap();
130        let deserialized: Layer = serde_json::from_str(&json).unwrap();
131        assert_eq!(layer, deserialized);
132    }
133
134    #[test]
135    fn test_yaml_round_trip_connector() {
136        let conn = Connector::new(ConnectorStyle::Dots);
137        let layer = Layer::Connector(conn);
138        let json = serde_json::to_string(&layer).unwrap();
139        let deserialized: Layer = serde_json::from_str(&json).unwrap();
140        assert_eq!(layer, deserialized);
141    }
142
143    #[test]
144    fn test_yaml_round_trip_flow_labels() {
145        let labels = FlowLabels::new(vec!["test".to_owned()]);
146        let layer = Layer::FlowLabels(labels);
147        let json = serde_json::to_string(&layer).unwrap();
148        let deserialized: Layer = serde_json::from_str(&json).unwrap();
149        assert_eq!(layer, deserialized);
150    }
151
152    #[test]
153    fn test_deserialize_unknown_variant() {
154        let json = r#"{"unknown": {}}"#;
155        let err = serde_json::from_str::<Layer>(json).unwrap_err();
156        let msg = format!("{err}");
157        assert!(msg.contains("unknown"));
158    }
159
160    #[test]
161    fn test_deserialize_empty_map_rejected() {
162        let json = "{}";
163        let err = serde_json::from_str::<Layer>(json).unwrap_err();
164        let msg = format!("{err}");
165        assert!(msg.contains("expected a layer variant key"));
166    }
167
168    #[test]
169    fn test_flow_labels_items_accessor() {
170        let labels = FlowLabels::new(vec!["a".to_owned(), "b".to_owned(), "c".to_owned()]);
171        assert_eq!(labels.items().len(), 3);
172        assert_eq!(labels.items()[0], "a");
173        assert_eq!(labels.items()[2], "c");
174    }
175
176    #[test]
177    fn test_flow_labels_serde_round_trip() {
178        let labels = FlowLabels::new(vec!["SQL queries".to_owned()]);
179        let json = serde_json::to_string(&labels).unwrap();
180        let deserialized: FlowLabels = serde_json::from_str(&json).unwrap();
181        assert_eq!(labels, deserialized);
182    }
183
184    #[test]
185    fn test_layer_debug() {
186        let labels = FlowLabels::new(vec!["test".to_owned()]);
187        let layer = Layer::FlowLabels(labels);
188        let debug = format!("{layer:?}");
189        assert!(debug.contains("FlowLabels"));
190    }
191
192    #[test]
193    fn test_layer_clone_eq() {
194        let conn = Connector::new(ConnectorStyle::Line);
195        let layer = Layer::Connector(conn);
196        let cloned = layer.clone();
197        assert_eq!(layer, cloned);
198    }
199
200    #[test]
201    fn test_deserialize_from_non_map_type_rejected() {
202        // Try to deserialize from a JSON array (not a map)
203        let json = r#"["tier", {}]"#;
204        let err = serde_json::from_str::<Layer>(json).unwrap_err();
205        let msg = format!("{err}");
206        // The expecting() method should describe what was expected
207        assert!(
208            msg.contains("tier")
209                || msg.contains("connector")
210                || msg.contains("flow_labels")
211                || msg.contains("map")
212                || msg.contains("invalid type")
213        );
214    }
215
216    #[test]
217    fn test_deserialize_from_string_rejected() {
218        let json = r#""tier""#;
219        let err = serde_json::from_str::<Layer>(json).unwrap_err();
220        let msg = format!("{err}");
221        assert!(msg.contains("invalid type") || msg.contains("map"));
222    }
223
224    #[test]
225    fn test_flow_labels_empty() {
226        let labels = FlowLabels::new(vec![]);
227        assert!(labels.items().is_empty());
228    }
229}