1use std::collections::HashSet;
4
5use serde::{Deserialize, Serialize};
6
7use crate::edge::Edge;
8use crate::error::ValidationError;
9use crate::id::NodeId;
10use crate::layer::Layer;
11use crate::legend::LegendEntry;
12
13const MAX_NESTING_DEPTH: usize = 3;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub struct Title {
20 text: String,
22 accent: String,
24}
25
26impl Title {
27 pub fn new(text: &str, accent: &str) -> Self {
29 Self {
30 text: text.to_owned(),
31 accent: accent.to_owned(),
32 }
33 }
34
35 pub fn text(&self) -> &str {
37 &self.text
38 }
39
40 pub fn accent(&self) -> &str {
42 &self.accent
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub struct DiagramHeader {
50 title: Title,
51 subtitle: String,
52 theme: String,
53}
54
55impl DiagramHeader {
56 pub fn new(title: Title, subtitle: &str, theme: &str) -> Self {
58 Self {
59 title,
60 subtitle: subtitle.to_owned(),
61 theme: theme.to_owned(),
62 }
63 }
64
65 pub fn title(&self) -> &Title {
67 &self.title
68 }
69
70 pub fn subtitle(&self) -> &str {
72 &self.subtitle
73 }
74
75 pub fn theme(&self) -> &str {
77 &self.theme
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub struct RawDiagram {
88 pub diagram: DiagramHeader,
90 pub layers: Vec<Layer>,
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub legend: Vec<LegendEntry>,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub edges: Vec<Edge>,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize)]
110#[serde(transparent)]
111pub struct Diagram(RawDiagram);
112
113impl Diagram {
114 pub fn header(&self) -> &DiagramHeader {
116 &self.0.diagram
117 }
118
119 pub fn layers(&self) -> &[Layer] {
121 &self.0.layers
122 }
123
124 pub fn legend(&self) -> &[LegendEntry] {
126 &self.0.legend
127 }
128
129 pub fn edges(&self) -> &[Edge] {
131 &self.0.edges
132 }
133
134 fn collect_all_node_ids(layers: &[Layer]) -> Vec<&NodeId> {
136 let mut ids = Vec::new();
137 Self::collect_node_ids_recursive(layers, &mut ids);
138 ids
139 }
140
141 fn collect_node_ids_recursive<'a>(layers: &'a [Layer], ids: &mut Vec<&'a NodeId>) {
142 for layer in layers {
143 if let Layer::Tier(tier) = layer {
144 for node in tier.nodes() {
145 ids.push(node.id());
146 }
147 if let Some(container) = tier.container() {
148 Self::collect_node_ids_recursive(container.layers(), ids);
149 }
150 }
151 }
152 }
153
154 fn check_empty_tiers(layers: &[Layer]) -> Result<(), ValidationError> {
156 for layer in layers {
157 if let Layer::Tier(tier) = layer {
158 if tier.is_empty() {
159 return Err(ValidationError::EmptyTier {
160 id: tier.id().to_string(),
161 });
162 }
163 if let Some(container) = tier.container() {
164 Self::check_empty_tiers(container.layers())?;
165 }
166 }
167 }
168 Ok(())
169 }
170
171 fn check_nesting_depth(layers: &[Layer], current_depth: usize) -> Result<(), ValidationError> {
173 for layer in layers {
174 if let Layer::Tier(tier) = layer {
175 if let Some(container) = tier.container() {
176 let depth = current_depth + 1;
177 if depth > MAX_NESTING_DEPTH {
178 return Err(ValidationError::NestingTooDeep {
179 max_depth: MAX_NESTING_DEPTH,
180 actual_depth: depth,
181 });
182 }
183 Self::check_nesting_depth(container.layers(), depth)?;
184 }
185 }
186 }
187 Ok(())
188 }
189}
190
191impl TryFrom<RawDiagram> for Diagram {
192 type Error = ValidationError;
193
194 fn try_from(raw: RawDiagram) -> Result<Self, Self::Error> {
195 Self::check_empty_tiers(&raw.layers)?;
197
198 Self::check_nesting_depth(&raw.layers, 0)?;
200
201 let all_ids = Self::collect_all_node_ids(&raw.layers);
203 let mut seen = HashSet::new();
204 for id in &all_ids {
205 if !seen.insert(id.as_str()) {
206 return Err(ValidationError::DuplicateNodeId { id: id.to_string() });
207 }
208 }
209
210 for edge in &raw.edges {
212 if !seen.contains(edge.from_id().as_str()) {
213 return Err(ValidationError::DanglingEdgeReference {
214 id: edge.from_id().to_string(),
215 field: "from",
216 });
217 }
218 if !seen.contains(edge.to_id().as_str()) {
219 return Err(ValidationError::DanglingEdgeReference {
220 id: edge.to_id().to_string(),
221 field: "to",
222 });
223 }
224 }
225
226 Ok(Self(raw))
227 }
228}
229
230impl<'de> Deserialize<'de> for Diagram {
231 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
232 where
233 D: serde::Deserializer<'de>,
234 {
235 let raw = RawDiagram::deserialize(deserializer)?;
236 Diagram::try_from(raw).map_err(serde::de::Error::custom)
237 }
238}
239
240#[cfg(test)]
242fn test_node(id: &str) -> crate::node::Node {
243 crate::node::Node::builder()
244 .id(NodeId::new(id).unwrap())
245 .kind(crate::kind::NodeKind::System)
246 .color(crate::color::Color::Blue)
247 .icon("◇")
248 .title(id)
249 .description("test node")
250 .build()
251 .unwrap()
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::color::Color;
258 use crate::connector::{Connector, ConnectorStyle};
259 use crate::container::{Container, ContainerBorder};
260 use crate::kind::EdgeKind;
261 use crate::layer::Layer;
262 use crate::node::Node;
263 use crate::tier::Tier;
264
265 fn simple_raw(nodes: Vec<Node>, edges: Vec<Edge>) -> RawDiagram {
266 RawDiagram {
267 diagram: DiagramHeader::new(Title::new("test", "test"), "a test diagram", "dark"),
268 layers: vec![Layer::Tier(Tier::new(NodeId::new("main").unwrap(), nodes))],
269 legend: vec![],
270 edges,
271 }
272 }
273
274 #[test]
275 fn test_valid_diagram() {
276 let raw = simple_raw(
277 vec![test_node("app"), test_node("db")],
278 vec![Edge::new(
279 NodeId::new("app").unwrap(),
280 NodeId::new("db").unwrap(),
281 EdgeKind::Uses,
282 )],
283 );
284 let diagram = Diagram::try_from(raw);
285 assert!(diagram.is_ok());
286 }
287
288 #[test]
289 fn test_duplicate_node_id_rejected() {
290 let raw = simple_raw(vec![test_node("app"), test_node("app")], vec![]);
291 let err = Diagram::try_from(raw).unwrap_err();
292 assert!(matches!(err, ValidationError::DuplicateNodeId { id } if id == "app"));
293 }
294
295 #[test]
296 fn test_dangling_edge_from_rejected() {
297 let raw = simple_raw(
298 vec![test_node("app")],
299 vec![Edge::new(
300 NodeId::new("ghost").unwrap(),
301 NodeId::new("app").unwrap(),
302 EdgeKind::Uses,
303 )],
304 );
305 let err = Diagram::try_from(raw).unwrap_err();
306 assert!(
307 matches!(err, ValidationError::DanglingEdgeReference { id, field } if id == "ghost" && field == "from")
308 );
309 }
310
311 #[test]
312 fn test_dangling_edge_to_rejected() {
313 let raw = simple_raw(
314 vec![test_node("app")],
315 vec![Edge::new(
316 NodeId::new("app").unwrap(),
317 NodeId::new("ghost").unwrap(),
318 EdgeKind::Uses,
319 )],
320 );
321 let err = Diagram::try_from(raw).unwrap_err();
322 assert!(
323 matches!(err, ValidationError::DanglingEdgeReference { id, field } if id == "ghost" && field == "to")
324 );
325 }
326
327 #[test]
328 fn test_empty_tier_rejected() {
329 let raw = RawDiagram {
330 diagram: DiagramHeader::new(Title::new("test", "test"), "test", "dark"),
331 layers: vec![Layer::Tier(Tier::new(
332 NodeId::new("empty").unwrap(),
333 vec![],
334 ))],
335 legend: vec![],
336 edges: vec![],
337 };
338 let err = Diagram::try_from(raw).unwrap_err();
339 assert!(matches!(err, ValidationError::EmptyTier { id } if id == "empty"));
340 }
341
342 #[test]
343 fn test_tier_with_container_not_empty() {
344 let container = Container::new(
345 "server",
346 ContainerBorder::Solid,
347 Color::Green,
348 vec![Layer::Tier(Tier::new(
349 NodeId::new("inner").unwrap(),
350 vec![test_node("api")],
351 ))],
352 );
353 let raw = RawDiagram {
354 diagram: DiagramHeader::new(Title::new("test", "test"), "test", "dark"),
355 layers: vec![Layer::Tier(Tier::with_container(
356 NodeId::new("server").unwrap(),
357 container,
358 ))],
359 legend: vec![],
360 edges: vec![],
361 };
362 assert!(Diagram::try_from(raw).is_ok());
363 }
364
365 #[test]
366 fn test_connector_layer_does_not_affect_validation() {
367 let raw = RawDiagram {
368 diagram: DiagramHeader::new(Title::new("test", "test"), "test", "dark"),
369 layers: vec![
370 Layer::Tier(Tier::new(NodeId::new("top").unwrap(), vec![test_node("a")])),
371 Layer::Connector(Connector::with_label(ConnectorStyle::Line, "HTTPS")),
372 Layer::Tier(Tier::new(
373 NodeId::new("bottom").unwrap(),
374 vec![test_node("b")],
375 )),
376 ],
377 legend: vec![],
378 edges: vec![],
379 };
380 assert!(Diagram::try_from(raw).is_ok());
381 }
382
383 #[test]
384 fn test_serde_round_trip() {
385 let raw = simple_raw(vec![test_node("app")], vec![]);
386 let diagram = Diagram::try_from(raw).unwrap();
387 let json = serde_json::to_string_pretty(&diagram).unwrap();
388 let deserialized: Diagram = serde_json::from_str(&json).unwrap();
389 assert_eq!(diagram, deserialized);
390 }
391
392 #[test]
393 fn test_nesting_too_deep_rejected() {
394 let deepest_tier = Tier::new(NodeId::new("deep").unwrap(), vec![test_node("d")]);
397 let level4 = Container::new(
398 "l4",
399 ContainerBorder::Dashed,
400 Color::Blue,
401 vec![Layer::Tier(deepest_tier)],
402 );
403 let level3 = Container::new(
404 "l3",
405 ContainerBorder::Dashed,
406 Color::Blue,
407 vec![Layer::Tier(Tier::with_container(
408 NodeId::new("l3t").unwrap(),
409 level4,
410 ))],
411 );
412 let level2 = Container::new(
413 "l2",
414 ContainerBorder::Dashed,
415 Color::Blue,
416 vec![Layer::Tier(Tier::with_container(
417 NodeId::new("l2t").unwrap(),
418 level3,
419 ))],
420 );
421 let level1 = Container::new(
422 "l1",
423 ContainerBorder::Dashed,
424 Color::Blue,
425 vec![Layer::Tier(Tier::with_container(
426 NodeId::new("l1t").unwrap(),
427 level2,
428 ))],
429 );
430 let top_tier = Tier::with_container(NodeId::new("top").unwrap(), level1);
431
432 let raw = RawDiagram {
433 diagram: DiagramHeader::new(Title::new("test", "test"), "test", "dark"),
434 layers: vec![Layer::Tier(top_tier)],
435 legend: vec![],
436 edges: vec![],
437 };
438 let err = Diagram::try_from(raw).unwrap_err();
439 assert!(matches!(err, ValidationError::NestingTooDeep { .. }));
440 }
441
442 #[test]
443 fn test_diagram_accessors() {
444 let raw = simple_raw(
445 vec![test_node("app")],
446 vec![Edge::new(
447 NodeId::new("app").unwrap(),
448 NodeId::new("app").unwrap(),
449 EdgeKind::Uses,
450 )],
451 );
452 let diagram = Diagram::try_from(raw).unwrap();
453
454 assert_eq!(diagram.header().title().text(), "test");
455 assert_eq!(diagram.header().title().accent(), "test");
456 assert_eq!(diagram.header().subtitle(), "a test diagram");
457 assert_eq!(diagram.header().theme(), "dark");
458 assert_eq!(diagram.layers().len(), 1);
459 assert_eq!(diagram.edges().len(), 1);
460 assert!(diagram.legend().is_empty());
461 }
462
463 #[test]
464 fn test_nested_container_node_ids_collected() {
465 let container = Container::new(
466 "server",
467 ContainerBorder::Solid,
468 Color::Green,
469 vec![Layer::Tier(Tier::new(
470 NodeId::new("inner").unwrap(),
471 vec![test_node("api")],
472 ))],
473 );
474 let raw = RawDiagram {
475 diagram: DiagramHeader::new(Title::new("test", "test"), "test", "dark"),
476 layers: vec![Layer::Tier(Tier::with_container(
477 NodeId::new("server").unwrap(),
478 container,
479 ))],
480 legend: vec![],
481 edges: vec![Edge::new(
482 NodeId::new("api").unwrap(),
483 NodeId::new("api").unwrap(),
484 EdgeKind::Uses,
485 )],
486 };
487 assert!(Diagram::try_from(raw).is_ok());
489 }
490}