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}