Skip to main content

frequenz_microgrid_component_graph/
error.rs

1// License: MIT
2// Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3
4//! This module defines the [`Error`] struct and the [`ErrorKind`] enum, which
5//! represent errors that can occur in the library, along with
6//! [`ValidationError`] for the individual failures collected while validating a
7//! graph.
8
9/// The kind of an [`Error`].
10///
11/// Marked `#[non_exhaustive]`: matching on this enum from outside the crate
12/// must include a wildcard arm, so future kinds can be added without a
13/// breaking change.
14#[derive(Debug, Clone, PartialEq)]
15#[non_exhaustive]
16pub enum ErrorKind {
17    /// No component was found for a given component ID.
18    ComponentNotFound,
19
20    /// An internal invariant of the library was violated. This indicates a bug.
21    Internal,
22
23    /// A component is invalid, e.g. it has an unspecified category.
24    InvalidComponent,
25
26    /// A connection between two components is invalid.
27    InvalidConnection,
28
29    /// The graph is structurally invalid, e.g. it has no grid component,
30    /// several grid components, or a duplicate component ID.
31    InvalidGraph,
32
33    /// One or more checks failed while validating an otherwise well-formed
34    /// graph. Holds every [`ValidationError`] that was collected.
35    ValidationErrors(Vec<ValidationError>),
36}
37
38impl std::fmt::Display for ErrorKind {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        let name = match self {
41            Self::ComponentNotFound => "ComponentNotFound",
42            Self::Internal => "Internal",
43            Self::InvalidComponent => "InvalidComponent",
44            Self::InvalidConnection => "InvalidConnection",
45            Self::InvalidGraph => "InvalidGraph",
46            Self::ValidationErrors(_) => "ValidationErrors",
47        };
48        f.write_str(name)
49    }
50}
51
52/// An error that can occur during the creation or traversal of a
53/// [ComponentGraph][crate::ComponentGraph].
54#[derive(Debug, Clone, PartialEq)]
55pub struct Error {
56    kind: ErrorKind,
57    desc: String,
58}
59
60impl Error {
61    /// Returns the [`ErrorKind`] of this error.
62    pub fn kind(&self) -> &ErrorKind {
63        &self.kind
64    }
65
66    /// If this is an [`ErrorKind::ValidationErrors`], returns the collected
67    /// failures; otherwise returns the error unchanged. Any other kind arising
68    /// during validation signals an internal inconsistency rather than a
69    /// topology failure (e.g. a graph lookup returning `ComponentNotFound`), so
70    /// it is propagated to abort validation rather than collected.
71    pub(crate) fn into_validation_errors(self) -> Result<Vec<ValidationError>, Error> {
72        match self.kind {
73            ErrorKind::ValidationErrors(errors) => Ok(errors),
74            kind => Err(Error {
75                kind,
76                desc: self.desc,
77            }),
78        }
79    }
80}
81
82/// Constructors for [`Error`].
83impl Error {
84    /// Creates a new [`Error`] with the `ComponentNotFound` kind and the given
85    /// description.
86    pub(crate) fn component_not_found(desc: impl Into<String>) -> Self {
87        Self {
88            kind: ErrorKind::ComponentNotFound,
89            desc: desc.into(),
90        }
91    }
92
93    /// Creates a new [`Error`] with the `Internal` kind and the given
94    /// description.
95    pub(crate) fn internal(desc: impl Into<String>) -> Self {
96        Self {
97            kind: ErrorKind::Internal,
98            desc: desc.into(),
99        }
100    }
101
102    /// Creates a new [`Error`] with the `InvalidComponent` kind and the given
103    /// description.
104    pub(crate) fn invalid_component(desc: impl Into<String>) -> Self {
105        Self {
106            kind: ErrorKind::InvalidComponent,
107            desc: desc.into(),
108        }
109    }
110
111    /// Creates a new [`Error`] with the `InvalidConnection` kind and the given
112    /// description.
113    pub(crate) fn invalid_connection(desc: impl Into<String>) -> Self {
114        Self {
115            kind: ErrorKind::InvalidConnection,
116            desc: desc.into(),
117        }
118    }
119
120    /// Creates a new [`Error`] with the `InvalidGraph` kind and the given
121    /// description.
122    pub(crate) fn invalid_graph(desc: impl Into<String>) -> Self {
123        Self {
124            kind: ErrorKind::InvalidGraph,
125            desc: desc.into(),
126        }
127    }
128
129    /// Creates a new [`Error`] with the `ValidationErrors` kind from the
130    /// collected validation failures.
131    pub(crate) fn validation_errors(errors: Vec<ValidationError>) -> Self {
132        // The failures carry their own descriptions, so `desc` is unused for
133        // this kind; `Display` formats the collected errors directly.
134        Self {
135            kind: ErrorKind::ValidationErrors(errors),
136            desc: String::new(),
137        }
138    }
139}
140
141impl std::fmt::Display for Error {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match &self.kind {
144            ErrorKind::ValidationErrors(errors) => {
145                write!(f, "Graph validation failed:")?;
146                for error in errors {
147                    write!(f, "\n    {error}")?;
148                }
149                Ok(())
150            }
151            kind => write!(f, "{kind}: {}", self.desc),
152        }
153    }
154}
155
156impl std::error::Error for Error {}
157
158/// A single failure found while validating a
159/// [`ComponentGraph`][crate::ComponentGraph]'s topology.
160///
161/// Validation collects every failure rather than stopping at the first one, so
162/// a single graph can yield many of these (see [`ErrorKind::ValidationErrors`]).
163/// Besides a human-readable [`message`][Self::message], each failure exposes the
164/// [`component_ids`][Self::component_ids] it involves, so callers can act on the
165/// affected components without parsing the message text.
166#[derive(Debug, Clone, PartialEq)]
167pub struct ValidationError {
168    message: String,
169    component_ids: Vec<u64>,
170}
171
172impl ValidationError {
173    /// Creates a new validation error from a message and the IDs of the
174    /// components it involves.
175    pub(crate) fn new(message: impl Into<String>, component_ids: impl Into<Vec<u64>>) -> Self {
176        Self {
177            message: message.into(),
178            component_ids: component_ids.into(),
179        }
180    }
181
182    /// A human-readable description of the failure.
183    pub fn message(&self) -> &str {
184        &self.message
185    }
186
187    /// The IDs of the components involved in the failure.
188    pub fn component_ids(&self) -> &[u64] {
189        &self.component_ids
190    }
191}
192
193impl std::fmt::Display for ValidationError {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        f.write_str(&self.message)
196    }
197}
198
199impl std::error::Error for ValidationError {}
200
201impl From<ValidationError> for Error {
202    fn from(error: ValidationError) -> Self {
203        Error::validation_errors(vec![error])
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn validation_error_exposes_its_message_and_components() {
213        let error = ValidationError::new("boom", [3u64, 4]);
214        assert_eq!(error.message(), "boom");
215        assert_eq!(error.component_ids(), &[3, 4]);
216        // `Display` is just the message.
217        assert_eq!(error.to_string(), "boom");
218    }
219
220    #[test]
221    fn leaf_error_display_is_unchanged() {
222        assert_eq!(
223            Error::invalid_graph("No grid component found.").to_string(),
224            "InvalidGraph: No grid component found."
225        );
226    }
227
228    #[test]
229    fn validation_errors_display_lists_each_failure() {
230        let error = Error::validation_errors(vec![
231            ValidationError::new("first problem", [1u64]),
232            ValidationError::new("second problem", [2u64, 3]),
233        ]);
234        assert_eq!(
235            error.to_string(),
236            "Graph validation failed:\n    first problem\n    second problem"
237        );
238    }
239
240    #[test]
241    fn into_validation_errors_unwraps_the_collected_failures() {
242        let error: Error = ValidationError::new("boom", [1u64]).into();
243        assert_eq!(
244            error.into_validation_errors(),
245            Ok(vec![ValidationError::new("boom", [1u64])])
246        );
247    }
248
249    #[test]
250    fn into_validation_errors_passes_other_kinds_through() {
251        assert_eq!(
252            Error::internal("bug").into_validation_errors(),
253            Err(Error::internal("bug"))
254        );
255    }
256}