1use serde::{Deserialize, Serialize};
4
5use crate::color::Color;
6use crate::error::ValidationError;
7use crate::id::NodeId;
8use crate::kind::NodeKind;
9use crate::metadata::Metadata;
10use crate::tech::Tech;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub struct Node {
22 id: NodeId,
23 kind: NodeKind,
24 color: Color,
25 icon: String,
26 title: String,
27 description: String,
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 tech: Vec<Tech>,
30 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
31 metadata: Metadata,
32}
33
34impl Node {
35 pub fn builder() -> NodeBuilder {
37 NodeBuilder::default()
38 }
39
40 pub fn id(&self) -> &NodeId {
42 &self.id
43 }
44
45 pub fn kind(&self) -> NodeKind {
47 self.kind
48 }
49
50 pub fn color(&self) -> Color {
52 self.color
53 }
54
55 pub fn icon(&self) -> &str {
57 &self.icon
58 }
59
60 pub fn title(&self) -> &str {
62 &self.title
63 }
64
65 pub fn description(&self) -> &str {
67 &self.description
68 }
69
70 pub fn tech(&self) -> &[Tech] {
72 &self.tech
73 }
74
75 pub fn metadata(&self) -> &Metadata {
77 &self.metadata
78 }
79}
80
81#[derive(Debug, Default)]
104pub struct NodeBuilder {
105 id: Option<NodeId>,
106 kind: Option<NodeKind>,
107 color: Option<Color>,
108 icon: Option<String>,
109 title: Option<String>,
110 description: Option<String>,
111 tech: Vec<Tech>,
112 metadata: Metadata,
113}
114
115impl NodeBuilder {
116 pub fn id(mut self, id: NodeId) -> Self {
118 self.id = Some(id);
119 self
120 }
121
122 pub fn kind(mut self, kind: NodeKind) -> Self {
124 self.kind = Some(kind);
125 self
126 }
127
128 pub fn color(mut self, color: Color) -> Self {
130 self.color = Some(color);
131 self
132 }
133
134 pub fn icon(mut self, icon: &str) -> Self {
136 self.icon = Some(icon.to_owned());
137 self
138 }
139
140 pub fn title(mut self, title: &str) -> Self {
142 self.title = Some(title.to_owned());
143 self
144 }
145
146 pub fn description(mut self, description: &str) -> Self {
148 self.description = Some(description.to_owned());
149 self
150 }
151
152 pub fn tech(mut self, tech: Vec<Tech>) -> Self {
154 self.tech = tech;
155 self
156 }
157
158 pub fn metadata(mut self, metadata: Metadata) -> Self {
160 self.metadata = metadata;
161 self
162 }
163
164 pub fn build(self) -> Result<Node, ValidationError> {
166 Ok(Node {
167 id: self
168 .id
169 .ok_or(ValidationError::MissingField { field: "id" })?,
170 kind: self
171 .kind
172 .ok_or(ValidationError::MissingField { field: "kind" })?,
173 color: self
174 .color
175 .ok_or(ValidationError::MissingField { field: "color" })?,
176 icon: self
177 .icon
178 .ok_or(ValidationError::MissingField { field: "icon" })?,
179 title: self
180 .title
181 .ok_or(ValidationError::MissingField { field: "title" })?,
182 description: self.description.ok_or(ValidationError::MissingField {
183 field: "description",
184 })?,
185 tech: self.tech,
186 metadata: self.metadata,
187 })
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn sample_node() -> Node {
196 Node::builder()
197 .id(NodeId::new("web-app").unwrap())
198 .kind(NodeKind::System)
199 .color(Color::Blue)
200 .icon("◇")
201 .title("Web Application")
202 .description("Browser-based frontend")
203 .tech(vec![Tech::new("React")])
204 .build()
205 .unwrap()
206 }
207
208 #[test]
209 fn test_builder_all_fields() {
210 let node = sample_node();
211 assert_eq!(node.id().as_str(), "web-app");
212 assert_eq!(node.kind(), NodeKind::System);
213 assert_eq!(node.color(), Color::Blue);
214 assert_eq!(node.icon(), "◇");
215 assert_eq!(node.title(), "Web Application");
216 assert_eq!(node.description(), "Browser-based frontend");
217 assert_eq!(node.tech().len(), 1);
218 }
219
220 #[test]
221 fn test_builder_missing_id() {
222 let result = Node::builder()
223 .kind(NodeKind::System)
224 .color(Color::Blue)
225 .icon("x")
226 .title("Test")
227 .description("Test")
228 .build();
229 assert!(matches!(
230 result,
231 Err(ValidationError::MissingField { field: "id" })
232 ));
233 }
234
235 #[test]
236 fn test_builder_missing_kind() {
237 let result = Node::builder()
238 .id(NodeId::new("a").unwrap())
239 .color(Color::Blue)
240 .icon("x")
241 .title("Test")
242 .description("Test")
243 .build();
244 assert!(matches!(
245 result,
246 Err(ValidationError::MissingField { field: "kind" })
247 ));
248 }
249
250 #[test]
251 fn test_builder_missing_color() {
252 let result = Node::builder()
253 .id(NodeId::new("a").unwrap())
254 .kind(NodeKind::System)
255 .icon("x")
256 .title("Test")
257 .description("Test")
258 .build();
259 assert!(matches!(
260 result,
261 Err(ValidationError::MissingField { field: "color" })
262 ));
263 }
264
265 #[test]
266 fn test_builder_missing_icon() {
267 let result = Node::builder()
268 .id(NodeId::new("a").unwrap())
269 .kind(NodeKind::System)
270 .color(Color::Blue)
271 .title("Test")
272 .description("Test")
273 .build();
274 assert!(matches!(
275 result,
276 Err(ValidationError::MissingField { field: "icon" })
277 ));
278 }
279
280 #[test]
281 fn test_builder_missing_title() {
282 let result = Node::builder()
283 .id(NodeId::new("a").unwrap())
284 .kind(NodeKind::System)
285 .color(Color::Blue)
286 .icon("x")
287 .description("Test")
288 .build();
289 assert!(matches!(
290 result,
291 Err(ValidationError::MissingField { field: "title" })
292 ));
293 }
294
295 #[test]
296 fn test_builder_missing_description() {
297 let result = Node::builder()
298 .id(NodeId::new("a").unwrap())
299 .kind(NodeKind::System)
300 .color(Color::Blue)
301 .icon("x")
302 .title("Test")
303 .build();
304 assert!(matches!(
305 result,
306 Err(ValidationError::MissingField {
307 field: "description"
308 })
309 ));
310 }
311
312 #[test]
313 fn test_builder_with_metadata() {
314 let mut meta = Metadata::new();
315 meta.insert("owner", "team-a");
316 let node = Node::builder()
317 .id(NodeId::new("a").unwrap())
318 .kind(NodeKind::System)
319 .color(Color::Blue)
320 .icon("x")
321 .title("Test")
322 .description("Test")
323 .metadata(meta)
324 .build()
325 .unwrap();
326 assert_eq!(node.metadata().get("owner"), Some("team-a"));
327 }
328
329 #[test]
330 fn test_serde_round_trip() {
331 let node = sample_node();
332 let json = serde_json::to_string_pretty(&node).unwrap();
333 let deserialized: Node = serde_json::from_str(&json).unwrap();
334 assert_eq!(node, deserialized);
335 }
336
337 #[test]
338 fn test_node_debug() {
339 let node = sample_node();
340 let debug = format!("{node:?}");
341 assert!(debug.contains("web-app"));
342 }
343
344 #[test]
345 fn test_node_clone_eq() {
346 let node = sample_node();
347 let cloned = node.clone();
348 assert_eq!(node, cloned);
349 }
350}