1use thiserror::Error;
4
5#[derive(Debug, Error, Clone, PartialEq)]
7pub enum FlowError {
8 #[error("Node with ID '{id}' not found")]
9 NodeNotFound { id: String },
10
11 #[error("Edge with ID '{id}' not found")]
12 EdgeNotFound { id: String },
13
14 #[error("Duplicate node ID: '{id}'")]
15 DuplicateNodeId { id: String },
16
17 #[error("Duplicate edge ID: '{id}'")]
18 DuplicateEdgeId { id: String },
19
20 #[error("Invalid connection: {message}")]
21 InvalidConnection { message: String },
22
23 #[error("Self connection not allowed")]
24 SelfConnection,
25
26 #[error("Spatial index error: {message}")]
27 SpatialIndex { message: String },
28
29 #[error("Layout error: {message}")]
30 Layout { message: String },
31
32 #[error("Invalid position: x={x}, y={y}")]
33 InvalidPosition { x: f64, y: f64 },
34
35 #[error("Invalid size: width={width}, height={height}")]
36 InvalidSize { width: f64, height: f64 },
37
38 #[error("Serialization error: {message}")]
39 Serialization { message: String },
40
41 #[error("Invalid operation: {message}")]
42 InvalidOperation { message: String },
43
44 #[error("Handle not found: {handle_id}")]
45 HandleNotFound { handle_id: String },
46
47 #[error("Connection limit exceeded for handle '{handle_id}': {current}/{limit}")]
48 ConnectionLimitExceeded {
49 handle_id: String,
50 current: usize,
51 limit: usize,
52 },
53}
54
55impl FlowError {
56 pub fn node_not_found(id: impl Into<String>) -> Self {
58 Self::NodeNotFound { id: id.into() }
59 }
60
61 pub fn edge_not_found(id: impl Into<String>) -> Self {
63 Self::EdgeNotFound { id: id.into() }
64 }
65
66 pub fn duplicate_node_id(id: impl Into<String>) -> Self {
68 Self::DuplicateNodeId { id: id.into() }
69 }
70
71 pub fn duplicate_edge_id(id: impl Into<String>) -> Self {
73 Self::DuplicateEdgeId { id: id.into() }
74 }
75
76 pub fn invalid_connection(message: impl Into<String>) -> Self {
78 Self::InvalidConnection {
79 message: message.into(),
80 }
81 }
82
83 pub fn spatial_index(message: impl Into<String>) -> Self {
85 Self::SpatialIndex {
86 message: message.into(),
87 }
88 }
89
90 pub fn layout(message: impl Into<String>) -> Self {
92 Self::Layout {
93 message: message.into(),
94 }
95 }
96
97 pub fn invalid_operation(message: impl Into<String>) -> Self {
99 Self::InvalidOperation {
100 message: message.into(),
101 }
102 }
103
104 pub fn handle_not_found(handle_id: impl Into<String>) -> Self {
106 Self::HandleNotFound {
107 handle_id: handle_id.into(),
108 }
109 }
110
111 pub fn connection_limit_exceeded(
113 handle_id: impl Into<String>,
114 current: usize,
115 limit: usize,
116 ) -> Self {
117 Self::ConnectionLimitExceeded {
118 handle_id: handle_id.into(),
119 current,
120 limit,
121 }
122 }
123}
124
125#[cfg(feature = "serde")]
126impl serde::Serialize for FlowError {
127 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
128 where
129 S: serde::Serializer,
130 {
131 serializer.serialize_str(&self.to_string())
132 }
133}
134
135pub type Result<T> = std::result::Result<T, FlowError>;
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::error::Error;
142
143 #[test]
146 fn test_node_not_found_constructor() {
147 let error = FlowError::node_not_found("node-123");
148 match &error {
149 FlowError::NodeNotFound { id } => {
150 assert_eq!(id, "node-123");
151 }
152 _ => panic!("Expected NodeNotFound variant"),
153 }
154 assert_eq!(error.to_string(), "Node with ID 'node-123' not found");
155 }
156
157 #[test]
158 fn test_edge_not_found_constructor() {
159 let error = FlowError::edge_not_found("edge-456");
160 match &error {
161 FlowError::EdgeNotFound { id } => {
162 assert_eq!(id, "edge-456");
163 }
164 _ => panic!("Expected EdgeNotFound variant"),
165 }
166 assert_eq!(error.to_string(), "Edge with ID 'edge-456' not found");
167 }
168
169 #[test]
170 fn test_duplicate_node_id_constructor() {
171 let error = FlowError::duplicate_node_id("dup-node");
172 match &error {
173 FlowError::DuplicateNodeId { id } => {
174 assert_eq!(id, "dup-node");
175 }
176 _ => panic!("Expected DuplicateNodeId variant"),
177 }
178 assert_eq!(error.to_string(), "Duplicate node ID: 'dup-node'");
179 }
180
181 #[test]
182 fn test_duplicate_edge_id_constructor() {
183 let error = FlowError::duplicate_edge_id("dup-edge");
184 match &error {
185 FlowError::DuplicateEdgeId { id } => {
186 assert_eq!(id, "dup-edge");
187 }
188 _ => panic!("Expected DuplicateEdgeId variant"),
189 }
190 assert_eq!(error.to_string(), "Duplicate edge ID: 'dup-edge'");
191 }
192
193 #[test]
194 fn test_invalid_connection_constructor() {
195 let error = FlowError::invalid_connection("Type mismatch");
196 match &error {
197 FlowError::InvalidConnection { message } => {
198 assert_eq!(message, "Type mismatch");
199 }
200 _ => panic!("Expected InvalidConnection variant"),
201 }
202 assert_eq!(error.to_string(), "Invalid connection: Type mismatch");
203 }
204
205 #[test]
206 fn test_spatial_index_constructor() {
207 let error = FlowError::spatial_index("Out of bounds");
208 match &error {
209 FlowError::SpatialIndex { message } => {
210 assert_eq!(message, "Out of bounds");
211 }
212 _ => panic!("Expected SpatialIndex variant"),
213 }
214 assert_eq!(error.to_string(), "Spatial index error: Out of bounds");
215 }
216
217 #[test]
218 fn test_layout_constructor() {
219 let error = FlowError::layout("Circular dependency");
220 match &error {
221 FlowError::Layout { message } => {
222 assert_eq!(message, "Circular dependency");
223 }
224 _ => panic!("Expected Layout variant"),
225 }
226 assert_eq!(error.to_string(), "Layout error: Circular dependency");
227 }
228
229 #[test]
230 fn test_invalid_operation_constructor() {
231 let error = FlowError::invalid_operation("Operation not supported");
232 match &error {
233 FlowError::InvalidOperation { message } => {
234 assert_eq!(message, "Operation not supported");
235 }
236 _ => panic!("Expected InvalidOperation variant"),
237 }
238 assert_eq!(
239 error.to_string(),
240 "Invalid operation: Operation not supported"
241 );
242 }
243
244 #[test]
245 fn test_handle_not_found_constructor() {
246 let error = FlowError::handle_not_found("handle-789");
247 match &error {
248 FlowError::HandleNotFound { handle_id } => {
249 assert_eq!(handle_id, "handle-789");
250 }
251 _ => panic!("Expected HandleNotFound variant"),
252 }
253 assert_eq!(error.to_string(), "Handle not found: handle-789");
254 }
255
256 #[test]
257 fn test_connection_limit_exceeded_constructor() {
258 let error = FlowError::connection_limit_exceeded("output-handle", 3, 2);
259 match &error {
260 FlowError::ConnectionLimitExceeded {
261 handle_id,
262 current,
263 limit,
264 } => {
265 assert_eq!(handle_id, "output-handle");
266 assert_eq!(*current, 3);
267 assert_eq!(*limit, 2);
268 }
269 _ => panic!("Expected ConnectionLimitExceeded variant"),
270 }
271 assert_eq!(
272 error.to_string(),
273 "Connection limit exceeded for handle 'output-handle': 3/2"
274 );
275 }
276
277 #[test]
280 fn test_self_connection_variant() {
281 let error = FlowError::SelfConnection;
282 assert_eq!(error.to_string(), "Self connection not allowed");
283 }
284
285 #[test]
286 fn test_invalid_position_variant() {
287 let error = FlowError::InvalidPosition {
288 x: f64::NAN,
289 y: f64::INFINITY,
290 };
291 assert_eq!(error.to_string(), "Invalid position: x=NaN, y=inf");
292 }
293
294 #[test]
295 fn test_invalid_size_variant() {
296 let error = FlowError::InvalidSize {
297 width: -10.0,
298 height: 0.0,
299 };
300 assert_eq!(error.to_string(), "Invalid size: width=-10, height=0");
301 }
302
303 #[test]
304 fn test_serialization_variant() {
305 let error = FlowError::Serialization {
306 message: "JSON parse error".to_string(),
307 };
308 assert_eq!(error.to_string(), "Serialization error: JSON parse error");
309 }
310
311 #[test]
314 fn test_error_equality_comprehensive() {
315 let error1 = FlowError::node_not_found("test");
317 let error2 = FlowError::node_not_found("test");
318 assert_eq!(error1, error2);
319
320 let error3 = FlowError::node_not_found("other");
322 assert_ne!(error1, error3);
323
324 let error4 = FlowError::edge_not_found("test");
326 assert_ne!(error1, error4);
327
328 let error5 = FlowError::connection_limit_exceeded("handle", 5, 3);
330 let error6 = FlowError::connection_limit_exceeded("handle", 5, 3);
331 let error7 = FlowError::connection_limit_exceeded("handle", 4, 3);
332
333 assert_eq!(error5, error6);
334 assert_ne!(error5, error7);
335 }
336
337 #[test]
340 fn test_error_cloning() {
341 let original = FlowError::invalid_connection("Test message");
342 let cloned = original.clone();
343
344 assert_eq!(original, cloned);
345
346 match (&original, &cloned) {
348 (
349 FlowError::InvalidConnection { message: msg1 },
350 FlowError::InvalidConnection { message: msg2 },
351 ) => {
352 assert_eq!(msg1, msg2);
353 }
354 _ => panic!("Cloning changed error variant"),
355 }
356 }
357
358 #[test]
361 fn test_empty_string_parameters() {
362 let error1 = FlowError::node_not_found("");
363 assert_eq!(error1.to_string(), "Node with ID '' not found");
364
365 let error2 = FlowError::invalid_connection("");
366 assert_eq!(error2.to_string(), "Invalid connection: ");
367 }
368
369 #[test]
370 fn test_special_characters_in_ids() {
371 let special_id = "node-123_with.special@chars#$%";
372 let error = FlowError::duplicate_node_id(special_id);
373 assert!(error.to_string().contains(special_id));
374 }
375
376 #[test]
377 fn test_unicode_characters() {
378 let unicode_id = "节点-123-ñoño";
379 let error = FlowError::edge_not_found(unicode_id);
380 assert!(error.to_string().contains(unicode_id));
381 }
382
383 #[test]
384 fn test_very_long_strings() {
385 let long_id = "a".repeat(1000);
386 let error = FlowError::handle_not_found(&long_id);
387 assert!(error.to_string().contains(&long_id));
388 }
389
390 #[test]
391 fn test_connection_limit_boundary_values() {
392 let error1 = FlowError::connection_limit_exceeded("handle", 1, 0);
394 assert_eq!(
395 error1.to_string(),
396 "Connection limit exceeded for handle 'handle': 1/0"
397 );
398
399 let error2 = FlowError::connection_limit_exceeded("handle", usize::MAX, usize::MAX - 1);
401 assert!(error2
402 .to_string()
403 .contains(&format!("{}/{}", usize::MAX, usize::MAX - 1)));
404 }
405
406 #[test]
409 fn test_result_type_usage() {
410 fn returns_error() -> Result<String> {
411 Err(FlowError::node_not_found("missing"))
412 }
413
414 fn returns_success() -> Result<String> {
415 Ok("success".to_string())
416 }
417
418 match returns_error() {
420 Ok(_) => panic!("Expected error"),
421 Err(error) => {
422 assert_eq!(error, FlowError::node_not_found("missing"));
423 }
424 }
425
426 match returns_success() {
428 Ok(value) => assert_eq!(value, "success"),
429 Err(_) => panic!("Expected success"),
430 }
431 }
432
433 #[test]
436 fn test_debug_formatting() {
437 let error = FlowError::node_not_found("debug-test");
438 let debug_str = format!("{:?}", error);
439
440 assert!(debug_str.contains("NodeNotFound"));
442 assert!(debug_str.contains("debug-test"));
443 }
444
445 #[test]
448 fn test_error_categorization_by_domain() {
449 let graph_errors = vec![
451 FlowError::node_not_found("test"),
452 FlowError::edge_not_found("test"),
453 FlowError::duplicate_node_id("test"),
454 FlowError::duplicate_edge_id("test"),
455 ];
456
457 for error in graph_errors {
458 assert!(is_graph_structure_error(&error));
459 }
460
461 let connection_errors = vec![
463 FlowError::invalid_connection("test"),
464 FlowError::SelfConnection,
465 FlowError::connection_limit_exceeded("handle", 1, 0),
466 ];
467
468 for error in connection_errors {
469 assert!(is_connection_error(&error));
470 }
471
472 let validation_errors = vec![
474 FlowError::InvalidPosition {
475 x: f64::NAN,
476 y: 0.0,
477 },
478 FlowError::InvalidSize {
479 width: -1.0,
480 height: 0.0,
481 },
482 ];
483
484 for error in validation_errors {
485 assert!(is_validation_error(&error));
486 }
487 }
488
489 fn is_graph_structure_error(error: &FlowError) -> bool {
491 matches!(
492 error,
493 FlowError::NodeNotFound { .. }
494 | FlowError::EdgeNotFound { .. }
495 | FlowError::DuplicateNodeId { .. }
496 | FlowError::DuplicateEdgeId { .. }
497 )
498 }
499
500 fn is_connection_error(error: &FlowError) -> bool {
501 matches!(
502 error,
503 FlowError::InvalidConnection { .. }
504 | FlowError::SelfConnection
505 | FlowError::ConnectionLimitExceeded { .. }
506 )
507 }
508
509 fn is_validation_error(error: &FlowError) -> bool {
510 matches!(
511 error,
512 FlowError::InvalidPosition { .. } | FlowError::InvalidSize { .. }
513 )
514 }
515
516 #[cfg(feature = "serde")]
519 #[test]
520 fn test_error_serialization() {
521 let error = FlowError::node_not_found("serialize-test");
522 let serialized = serde_json::to_string(&error).expect("Serialization should work");
523
524 assert_eq!(serialized, "\"Node with ID 'serialize-test' not found\"");
526 }
527
528 #[cfg(feature = "serde")]
529 #[test]
530 fn test_complex_error_serialization() {
531 let error = FlowError::connection_limit_exceeded("output", 5, 3);
532 let serialized = serde_json::to_string(&error).expect("Serialization should work");
533
534 assert_eq!(
535 serialized,
536 "\"Connection limit exceeded for handle 'output': 5/3\""
537 );
538 }
539
540 #[test]
543 fn test_from_string_conversion() {
544 let error1 = FlowError::node_not_found(String::from("owned-string"));
545 assert_eq!(error1.to_string(), "Node with ID 'owned-string' not found");
546
547 let error2 = FlowError::invalid_connection("string-slice");
548 assert_eq!(error2.to_string(), "Invalid connection: string-slice");
549 }
550
551 #[test]
554 fn test_std_error_trait_integration() {
555 let error = FlowError::layout("test error");
556
557 let _: &dyn std::error::Error = &error;
559
560 assert!(error.source().is_none());
562 }
563}