flow_rs_core/
error.rs

1//! Error types for Leptos Flow operations
2
3use thiserror::Error;
4
5/// Main error type for flow operations
6#[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    /// Create a node not found error
57    pub fn node_not_found(id: impl Into<String>) -> Self {
58        Self::NodeNotFound { id: id.into() }
59    }
60
61    /// Create an edge not found error
62    pub fn edge_not_found(id: impl Into<String>) -> Self {
63        Self::EdgeNotFound { id: id.into() }
64    }
65
66    /// Create a duplicate node ID error
67    pub fn duplicate_node_id(id: impl Into<String>) -> Self {
68        Self::DuplicateNodeId { id: id.into() }
69    }
70
71    /// Create a duplicate edge ID error
72    pub fn duplicate_edge_id(id: impl Into<String>) -> Self {
73        Self::DuplicateEdgeId { id: id.into() }
74    }
75
76    /// Create an invalid connection error
77    pub fn invalid_connection(message: impl Into<String>) -> Self {
78        Self::InvalidConnection {
79            message: message.into(),
80        }
81    }
82
83    /// Create a spatial index error
84    pub fn spatial_index(message: impl Into<String>) -> Self {
85        Self::SpatialIndex {
86            message: message.into(),
87        }
88    }
89
90    /// Create a layout error
91    pub fn layout(message: impl Into<String>) -> Self {
92        Self::Layout {
93            message: message.into(),
94        }
95    }
96
97    /// Create an invalid operation error
98    pub fn invalid_operation(message: impl Into<String>) -> Self {
99        Self::InvalidOperation {
100            message: message.into(),
101        }
102    }
103
104    /// Create a handle not found error
105    pub fn handle_not_found(handle_id: impl Into<String>) -> Self {
106        Self::HandleNotFound {
107            handle_id: handle_id.into(),
108        }
109    }
110
111    /// Create a connection limit exceeded error
112    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
135/// Result type alias for flow operations
136pub type Result<T> = std::result::Result<T, FlowError>;
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::error::Error;
142
143    // === Constructor Method Tests ===
144
145    #[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    // === Direct Variant Construction Tests ===
278
279    #[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    // === Error Equality and Comparison Tests ===
312
313    #[test]
314    fn test_error_equality_comprehensive() {
315        // Same error types with same data should be equal
316        let error1 = FlowError::node_not_found("test");
317        let error2 = FlowError::node_not_found("test");
318        assert_eq!(error1, error2);
319
320        // Same error types with different data should not be equal
321        let error3 = FlowError::node_not_found("other");
322        assert_ne!(error1, error3);
323
324        // Different error types should not be equal
325        let error4 = FlowError::edge_not_found("test");
326        assert_ne!(error1, error4);
327
328        // Complex error types equality
329        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    // === Error Cloning Tests ===
338
339    #[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        // Ensure they're independent objects
347        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    // === Edge Cases and Boundary Conditions ===
359
360    #[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        // Zero limit
393        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        // Maximum values
400        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    // === Result Type Integration Tests ===
407
408    #[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        // Test error case
419        match returns_error() {
420            Ok(_) => panic!("Expected error"),
421            Err(error) => {
422                assert_eq!(error, FlowError::node_not_found("missing"));
423            }
424        }
425
426        // Test success case
427        match returns_success() {
428            Ok(value) => assert_eq!(value, "success"),
429            Err(_) => panic!("Expected success"),
430        }
431    }
432
433    // === Debug Formatting Tests ===
434
435    #[test]
436    fn test_debug_formatting() {
437        let error = FlowError::node_not_found("debug-test");
438        let debug_str = format!("{:?}", error);
439
440        // Debug format should contain variant name and data
441        assert!(debug_str.contains("NodeNotFound"));
442        assert!(debug_str.contains("debug-test"));
443    }
444
445    // === Error Categorization Tests ===
446
447    #[test]
448    fn test_error_categorization_by_domain() {
449        // Graph structure errors
450        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        // Connection errors
462        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        // Validation errors
473        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    // Helper functions for error categorization
490    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    // === Serde Integration Tests ===
517
518    #[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        // Should serialize as the error message string
525        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    // === From Trait Integration Tests ===
541
542    #[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    // === Integration with std::error::Error Tests ===
552
553    #[test]
554    fn test_std_error_trait_integration() {
555        let error = FlowError::layout("test error");
556
557        // Should implement std::error::Error
558        let _: &dyn std::error::Error = &error;
559
560        // Test error source (should be None for our simple errors)
561        assert!(error.source().is_none());
562    }
563}