use serde::{Deserialize, Serialize};
use crate::color::Color;
use crate::error::ValidationError;
use crate::id::NodeId;
use crate::kind::NodeKind;
use crate::metadata::Metadata;
use crate::tech::Tech;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Node {
id: NodeId,
kind: NodeKind,
color: Color,
icon: String,
title: String,
description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tech: Vec<Tech>,
#[serde(default, skip_serializing_if = "Metadata::is_empty")]
metadata: Metadata,
}
impl Node {
pub fn builder() -> NodeBuilder {
NodeBuilder::default()
}
pub fn id(&self) -> &NodeId {
&self.id
}
pub fn kind(&self) -> NodeKind {
self.kind
}
pub fn color(&self) -> Color {
self.color
}
pub fn icon(&self) -> &str {
&self.icon
}
pub fn title(&self) -> &str {
&self.title
}
pub fn description(&self) -> &str {
&self.description
}
pub fn tech(&self) -> &[Tech] {
&self.tech
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
}
#[derive(Debug, Default)]
pub struct NodeBuilder {
id: Option<NodeId>,
kind: Option<NodeKind>,
color: Option<Color>,
icon: Option<String>,
title: Option<String>,
description: Option<String>,
tech: Vec<Tech>,
metadata: Metadata,
}
impl NodeBuilder {
pub fn id(mut self, id: NodeId) -> Self {
self.id = Some(id);
self
}
pub fn kind(mut self, kind: NodeKind) -> Self {
self.kind = Some(kind);
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn icon(mut self, icon: &str) -> Self {
self.icon = Some(icon.to_owned());
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_owned());
self
}
pub fn description(mut self, description: &str) -> Self {
self.description = Some(description.to_owned());
self
}
pub fn tech(mut self, tech: Vec<Tech>) -> Self {
self.tech = tech;
self
}
pub fn metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
pub fn build(self) -> Result<Node, ValidationError> {
Ok(Node {
id: self
.id
.ok_or(ValidationError::MissingField { field: "id" })?,
kind: self
.kind
.ok_or(ValidationError::MissingField { field: "kind" })?,
color: self
.color
.ok_or(ValidationError::MissingField { field: "color" })?,
icon: self
.icon
.ok_or(ValidationError::MissingField { field: "icon" })?,
title: self
.title
.ok_or(ValidationError::MissingField { field: "title" })?,
description: self.description.ok_or(ValidationError::MissingField {
field: "description",
})?,
tech: self.tech,
metadata: self.metadata,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_node() -> Node {
Node::builder()
.id(NodeId::new("web-app").unwrap())
.kind(NodeKind::System)
.color(Color::Blue)
.icon("â—‡")
.title("Web Application")
.description("Browser-based frontend")
.tech(vec![Tech::new("React")])
.build()
.unwrap()
}
#[test]
fn test_builder_all_fields() {
let node = sample_node();
assert_eq!(node.id().as_str(), "web-app");
assert_eq!(node.kind(), NodeKind::System);
assert_eq!(node.color(), Color::Blue);
assert_eq!(node.icon(), "â—‡");
assert_eq!(node.title(), "Web Application");
assert_eq!(node.description(), "Browser-based frontend");
assert_eq!(node.tech().len(), 1);
}
#[test]
fn test_builder_missing_id() {
let result = Node::builder()
.kind(NodeKind::System)
.color(Color::Blue)
.icon("x")
.title("Test")
.description("Test")
.build();
assert!(matches!(
result,
Err(ValidationError::MissingField { field: "id" })
));
}
#[test]
fn test_builder_missing_kind() {
let result = Node::builder()
.id(NodeId::new("a").unwrap())
.color(Color::Blue)
.icon("x")
.title("Test")
.description("Test")
.build();
assert!(matches!(
result,
Err(ValidationError::MissingField { field: "kind" })
));
}
#[test]
fn test_builder_missing_color() {
let result = Node::builder()
.id(NodeId::new("a").unwrap())
.kind(NodeKind::System)
.icon("x")
.title("Test")
.description("Test")
.build();
assert!(matches!(
result,
Err(ValidationError::MissingField { field: "color" })
));
}
#[test]
fn test_builder_missing_icon() {
let result = Node::builder()
.id(NodeId::new("a").unwrap())
.kind(NodeKind::System)
.color(Color::Blue)
.title("Test")
.description("Test")
.build();
assert!(matches!(
result,
Err(ValidationError::MissingField { field: "icon" })
));
}
#[test]
fn test_builder_missing_title() {
let result = Node::builder()
.id(NodeId::new("a").unwrap())
.kind(NodeKind::System)
.color(Color::Blue)
.icon("x")
.description("Test")
.build();
assert!(matches!(
result,
Err(ValidationError::MissingField { field: "title" })
));
}
#[test]
fn test_builder_missing_description() {
let result = Node::builder()
.id(NodeId::new("a").unwrap())
.kind(NodeKind::System)
.color(Color::Blue)
.icon("x")
.title("Test")
.build();
assert!(matches!(
result,
Err(ValidationError::MissingField {
field: "description"
})
));
}
#[test]
fn test_builder_with_metadata() {
let mut meta = Metadata::new();
meta.insert("owner", "team-a");
let node = Node::builder()
.id(NodeId::new("a").unwrap())
.kind(NodeKind::System)
.color(Color::Blue)
.icon("x")
.title("Test")
.description("Test")
.metadata(meta)
.build()
.unwrap();
assert_eq!(node.metadata().get("owner"), Some("team-a"));
}
#[test]
fn test_serde_round_trip() {
let node = sample_node();
let json = serde_json::to_string_pretty(&node).unwrap();
let deserialized: Node = serde_json::from_str(&json).unwrap();
assert_eq!(node, deserialized);
}
#[test]
fn test_node_debug() {
let node = sample_node();
let debug = format!("{node:?}");
assert!(debug.contains("web-app"));
}
#[test]
fn test_node_clone_eq() {
let node = sample_node();
let cloned = node.clone();
assert_eq!(node, cloned);
}
}