Skip to main content

cobre_core/
error.rs

1//! Error types produced during `System` construction and validation.
2//!
3//! [`ValidationError`] is returned by the system builder when entity cross-references
4//! are invalid, duplicate IDs are detected, topology is malformed, or penalty
5//! configuration is invalid.
6
7use core::fmt;
8
9use crate::EntityId;
10
11/// Errors produced during System construction and validation.
12///
13/// Returned by the `System` builder when loading and validating entity collections.
14/// Each variant captures enough context to pinpoint the invalid input without
15/// requiring the caller to re-inspect the data.
16///
17/// # Examples
18///
19/// ```
20/// use cobre_core::{EntityId, ValidationError};
21///
22/// let err = ValidationError::DuplicateId {
23///     entity_type: "Bus",
24///     id: EntityId(1),
25/// };
26/// assert!(err.to_string().contains("Bus"));
27/// ```
28#[derive(Debug, Clone)]
29pub enum ValidationError {
30    /// A cross-reference field (e.g., `bus_id`, `downstream_id`) refers to
31    /// an entity ID that does not exist in the system.
32    InvalidReference {
33        /// The entity type containing the invalid reference.
34        source_entity_type: &'static str,
35        /// The ID of the entity containing the invalid reference.
36        source_id: EntityId,
37        /// The name of the field containing the invalid reference.
38        field_name: &'static str,
39        /// The referenced ID that does not exist.
40        referenced_id: EntityId,
41        /// The entity type that was expected.
42        expected_type: &'static str,
43    },
44    /// Duplicate entity ID within a single entity collection.
45    DuplicateId {
46        /// The entity type with the duplicated ID.
47        entity_type: &'static str,
48        /// The duplicated entity ID.
49        id: EntityId,
50    },
51    /// The hydro cascade contains a cycle.
52    CascadeCycle {
53        /// IDs of hydros forming the cycle.
54        cycle_ids: Vec<EntityId>,
55    },
56    /// A hydro's filling configuration is invalid.
57    InvalidFillingConfig {
58        /// The hydro whose filling configuration is invalid.
59        hydro_id: EntityId,
60        /// Human-readable explanation of why the configuration is invalid.
61        reason: String,
62    },
63    /// A bus has no connections (no lines, generators, or loads).
64    ///
65    /// Emitted by `cobre-io` validation.
66    DisconnectedBus {
67        /// The ID of the disconnected bus.
68        bus_id: EntityId,
69    },
70    /// Entity-level penalty value is invalid (e.g., negative cost).
71    ///
72    /// Emitted by `cobre-io` validation.
73    InvalidPenalty {
74        /// The entity type with the invalid penalty.
75        entity_type: &'static str,
76        /// The ID of the entity with the invalid penalty.
77        entity_id: EntityId,
78        /// The name of the penalty field that is invalid.
79        field_name: &'static str,
80        /// Human-readable explanation of why the penalty is invalid.
81        reason: String,
82    },
83}
84
85impl fmt::Display for ValidationError {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Self::InvalidReference {
89                source_entity_type,
90                source_id,
91                field_name,
92                referenced_id,
93                expected_type,
94            } => write!(
95                f,
96                "{source_entity_type} with id {source_id} has invalid cross-reference \
97                 in field '{field_name}': referenced {expected_type} id {referenced_id} does not exist"
98            ),
99            Self::DuplicateId { entity_type, id } => {
100                write!(f, "duplicate {entity_type} id: {id}")
101            }
102            Self::CascadeCycle { cycle_ids } => {
103                let ids = cycle_ids
104                    .iter()
105                    .map(EntityId::to_string)
106                    .collect::<Vec<_>>()
107                    .join(", ");
108                write!(f, "hydro cascade contains a cycle: [{ids}]")
109            }
110            Self::InvalidFillingConfig { hydro_id, reason } => {
111                write!(
112                    f,
113                    "hydro {hydro_id} has invalid filling configuration: {reason}"
114                )
115            }
116            Self::DisconnectedBus { bus_id } => {
117                write!(
118                    f,
119                    "bus {bus_id} is disconnected (no lines, generators, or loads)"
120                )
121            }
122            Self::InvalidPenalty {
123                entity_type,
124                entity_id,
125                field_name,
126                reason,
127            } => write!(
128                f,
129                "{entity_type} with id {entity_id} has invalid penalty in field '{field_name}': {reason}"
130            ),
131        }
132    }
133}
134
135impl std::error::Error for ValidationError {}
136
137#[cfg(test)]
138mod tests {
139    use super::ValidationError;
140    use crate::EntityId;
141
142    #[test]
143    fn test_display_invalid_reference() {
144        let err = ValidationError::InvalidReference {
145            source_entity_type: "Hydro",
146            source_id: EntityId(3),
147            field_name: "bus_id",
148            referenced_id: EntityId(99),
149            expected_type: "Bus",
150        };
151        let msg = err.to_string();
152        assert!(msg.contains("Hydro"), "missing source entity type: {msg}");
153        assert!(msg.contains("bus_id"), "missing field name: {msg}");
154        assert!(msg.contains("99"), "missing referenced id: {msg}");
155    }
156
157    #[test]
158    fn test_display_duplicate_id() {
159        let err = ValidationError::DuplicateId {
160            entity_type: "Thermal",
161            id: EntityId(5),
162        };
163        let msg = err.to_string();
164        assert!(msg.contains("Thermal"), "missing entity type: {msg}");
165        assert!(msg.contains('5'), "missing id: {msg}");
166    }
167
168    #[test]
169    fn test_display_cascade_cycle() {
170        let err = ValidationError::CascadeCycle {
171            cycle_ids: vec![EntityId(1), EntityId(2), EntityId(3)],
172        };
173        let msg = err.to_string();
174        assert!(msg.contains('1'), "missing id 1: {msg}");
175        assert!(msg.contains('2'), "missing id 2: {msg}");
176        assert!(msg.contains('3'), "missing id 3: {msg}");
177    }
178
179    #[test]
180    fn test_error_trait() {
181        let err = ValidationError::DisconnectedBus {
182            bus_id: EntityId(7),
183        };
184        // Verify ValidationError can be used as &dyn std::error::Error
185        let _: &dyn std::error::Error = &err;
186    }
187}