Skip to main content

dendryform_core/
layout.rs

1//! Tier layout configuration.
2
3use serde::de::{self, MapAccess, Visitor};
4use serde::{Deserialize, Deserializer, Serialize};
5
6/// How nodes within a tier are arranged.
7///
8/// In YAML, this can be a string (`single`, `auto`) or a map (`grid: { columns: 4 }`).
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "snake_case")]
11#[non_exhaustive]
12pub enum TierLayout {
13    /// Full-width, centered single node.
14    Single,
15    /// CSS grid with a fixed number of columns.
16    Grid {
17        /// The number of columns in the grid.
18        columns: u32,
19    },
20    /// Automatic layout (one column per node, up to 4).
21    #[default]
22    Auto,
23}
24
25impl<'de> Deserialize<'de> for TierLayout {
26    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
27    where
28        D: Deserializer<'de>,
29    {
30        deserializer.deserialize_any(TierLayoutVisitor)
31    }
32}
33
34struct TierLayoutVisitor;
35
36impl<'de> Visitor<'de> for TierLayoutVisitor {
37    type Value = TierLayout;
38
39    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.write_str("\"single\", \"auto\", or a map like {grid: {columns: N}}")
41    }
42
43    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
44        match value {
45            "single" => Ok(TierLayout::Single),
46            "auto" => Ok(TierLayout::Auto),
47            other => Err(de::Error::unknown_variant(
48                other,
49                &["single", "auto", "grid"],
50            )),
51        }
52    }
53
54    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
55    where
56        A: MapAccess<'de>,
57    {
58        let key: String = map
59            .next_key()?
60            .ok_or_else(|| de::Error::custom("expected a layout variant key"))?;
61
62        match key.as_str() {
63            "grid" => {
64                #[derive(Deserialize)]
65                struct GridData {
66                    columns: u32,
67                }
68                let data: GridData = map.next_value()?;
69                Ok(TierLayout::Grid {
70                    columns: data.columns,
71                })
72            }
73            "single" => {
74                // Allow map form: { single: null } or { single: {} }
75                let _: serde::de::IgnoredAny = map.next_value()?;
76                Ok(TierLayout::Single)
77            }
78            "auto" => {
79                let _: serde::de::IgnoredAny = map.next_value()?;
80                Ok(TierLayout::Auto)
81            }
82            other => Err(de::Error::unknown_variant(
83                other,
84                &["single", "auto", "grid"],
85            )),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_default_is_auto() {
96        assert_eq!(TierLayout::default(), TierLayout::Auto);
97    }
98
99    #[test]
100    fn test_serde_single() {
101        let layout = TierLayout::Single;
102        let json = serde_json::to_string(&layout).unwrap();
103        assert_eq!(json, "\"single\"");
104        let deserialized: TierLayout = serde_json::from_str(&json).unwrap();
105        assert_eq!(layout, deserialized);
106    }
107
108    #[test]
109    fn test_serde_grid() {
110        let layout = TierLayout::Grid { columns: 3 };
111        let json = serde_json::to_string(&layout).unwrap();
112        let deserialized: TierLayout = serde_json::from_str(&json).unwrap();
113        assert_eq!(layout, deserialized);
114    }
115
116    #[test]
117    fn test_yaml_single_string() {
118        let yaml = "single";
119        let layout: TierLayout = serde_yml::from_str(yaml).unwrap();
120        assert_eq!(layout, TierLayout::Single);
121    }
122
123    #[test]
124    fn test_yaml_auto_string() {
125        let yaml = "auto";
126        let layout: TierLayout = serde_yml::from_str(yaml).unwrap();
127        assert_eq!(layout, TierLayout::Auto);
128    }
129
130    #[test]
131    fn test_yaml_grid_map() {
132        let yaml = "grid:\n  columns: 4";
133        let layout: TierLayout = serde_yml::from_str(yaml).unwrap();
134        assert_eq!(layout, TierLayout::Grid { columns: 4 });
135    }
136
137    #[test]
138    fn test_yaml_single_map_form() {
139        let yaml = "single: ~";
140        let layout: TierLayout = serde_yml::from_str(yaml).unwrap();
141        assert_eq!(layout, TierLayout::Single);
142    }
143
144    #[test]
145    fn test_yaml_auto_map_form() {
146        let yaml = "auto: ~";
147        let layout: TierLayout = serde_yml::from_str(yaml).unwrap();
148        assert_eq!(layout, TierLayout::Auto);
149    }
150
151    #[test]
152    fn test_yaml_unknown_string_variant() {
153        let yaml = "\"unknown_variant\"";
154        let result: Result<TierLayout, _> = serde_yml::from_str(yaml);
155        assert!(result.is_err());
156    }
157
158    #[test]
159    fn test_yaml_unknown_map_variant() {
160        let yaml = "unknown_key:\n  foo: bar";
161        let result: Result<TierLayout, _> = serde_yml::from_str(yaml);
162        assert!(result.is_err());
163    }
164
165    #[test]
166    fn test_json_single_string() {
167        let json = "\"single\"";
168        let layout: TierLayout = serde_json::from_str(json).unwrap();
169        assert_eq!(layout, TierLayout::Single);
170    }
171
172    #[test]
173    fn test_json_auto_string() {
174        let json = "\"auto\"";
175        let layout: TierLayout = serde_json::from_str(json).unwrap();
176        assert_eq!(layout, TierLayout::Auto);
177    }
178
179    #[test]
180    fn test_json_grid_map() {
181        let json = r#"{"grid":{"columns":3}}"#;
182        let layout: TierLayout = serde_json::from_str(json).unwrap();
183        assert_eq!(layout, TierLayout::Grid { columns: 3 });
184    }
185
186    #[test]
187    fn test_debug() {
188        let layout = TierLayout::Grid { columns: 2 };
189        let debug = format!("{layout:?}");
190        assert!(debug.contains("Grid"));
191        assert!(debug.contains("2"));
192    }
193
194    #[test]
195    fn test_clone_eq() {
196        let a = TierLayout::Grid { columns: 4 };
197        let b = a.clone();
198        assert_eq!(a, b);
199    }
200
201    #[test]
202    fn test_serde_auto_round_trip() {
203        let layout = TierLayout::Auto;
204        let json = serde_json::to_string(&layout).unwrap();
205        assert_eq!(json, "\"auto\"");
206        let deserialized: TierLayout = serde_json::from_str(&json).unwrap();
207        assert_eq!(layout, deserialized);
208    }
209
210    #[test]
211    fn test_yaml_empty_map_rejected() {
212        let yaml = "{}";
213        let result: Result<TierLayout, _> = serde_yml::from_str(yaml);
214        assert!(result.is_err());
215    }
216}