Skip to main content

eml_nl/
error.rs

1use crate::{
2    io::{OwnedQualifiedName, Span},
3    utils::{AffiliationId, CandidateId},
4};
5
6/// Different kinds of errors that can occur during EML_NL processing.
7#[derive(thiserror::Error, Debug)]
8pub enum EMLErrorKind {
9    /// An error originanting from the XML parser
10    #[error("XML error: {0}")]
11    XmlError(#[from] quick_xml::Error),
12
13    /// An input/output error
14    #[error("I/O error: {0}")]
15    IoError(#[from] std::io::Error),
16
17    /// An error during escaping/unescaping XML content
18    #[error("Escape error: {0}")]
19    EscapeError(#[from] quick_xml::escape::EscapeError),
20
21    /// An error related to parsing XML attributes
22    #[error("Attribute error: {0}")]
23    AttributeError(#[from] quick_xml::events::attributes::AttrError),
24
25    /// An error related to XML encoding/decoding
26    #[error("Encoding error: {0}")]
27    EncodingError(#[from] quick_xml::encoding::EncodingError),
28
29    /// An error converting from UTF-8
30    #[error("UTF-8 conversion error: {0}")]
31    FromUtf8Error(#[from] std::string::FromUtf8Error),
32
33    /// An end element was found, but it was not the expected one
34    #[error("Unexpected end element")]
35    UnexpectedEndElement,
36
37    /// The end of the file was reached unexpectedly
38    #[error("Unexpected end of file")]
39    UnexpectedEof,
40
41    /// An unexpected parsing event was encountered during XML parsing
42    #[error("Unexpected parsing event encountered")]
43    UnexpectedEvent,
44
45    /// A required element was missing
46    #[error("Missing required element: {0}")]
47    MissingElement(OwnedQualifiedName),
48
49    /// An element existed, but it was empty or had no text content
50    #[error("Missing value for element: {0}")]
51    MissingElementValue(OwnedQualifiedName),
52
53    /// None of the choice elements were found
54    #[error("Missing any of these elements: {0:?}")]
55    MissingChoiceElements(Vec<OwnedQualifiedName>),
56
57    /// A required attribute was missing
58    #[error("Missing required attribute: {0}")]
59    MissingAttribute(OwnedQualifiedName),
60
61    /// An unexpected element was found
62    #[error("Unexpected element: {0} inside of {1}")]
63    UnexpectedElement(OwnedQualifiedName, OwnedQualifiedName),
64
65    /// A namespace was encountered that is not recognized
66    #[error("Unknown namespace: {0}")]
67    UnknownNamespace(String),
68
69    /// The root element was not named "EML"
70    #[error("Root element must be named EML")]
71    InvalidRootElement,
72
73    /// The EML schema version is not supported
74    #[error("Schema version '{0}' is not supported, only version '5' is supported")]
75    SchemaVersionNotSupported(String),
76
77    /// The document type is not recognized
78    #[error("Unknown document type: {0}")]
79    UnknownDocumentType(String),
80
81    /// The document type is invalid, a specific type was expected
82    #[error("Invalid document type: expected {0}, found {1}")]
83    InvalidDocumentType(&'static str, String),
84
85    /// An invalid value was encountered for a specific attribute/element
86    #[error("Invalid value for {0}: {1}")]
87    InvalidValue(
88        OwnedQualifiedName,
89        #[source] Box<dyn std::error::Error + Send + Sync + 'static>,
90    ),
91
92    /// An error occurred while converting a value to the parsed type
93    #[error("Error converting value: {0}")]
94    ValueConversionError(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
95
96    /// Attributes cannot have the default namespace
97    #[error("Attributes cannot have the default namespace")]
98    AttributeNamespaceError,
99
100    /// Elements cannot be in no namespace when a default namespace is defined
101    #[error("Elements cannot be in no namespace when a default namespace is defined")]
102    ElementNamespaceError,
103
104    /// The ContestIdentifier element is missing
105    #[error("Missing the ContestIdentifier element")]
106    MissingContenstIdentifier,
107
108    /// The ElectionDate element is used without using the kiesraad namespace
109    #[error("Used ElectionDate element without using the kiesraad namespace")]
110    InvalidElectionDateNamespace,
111
112    /// The RejectedVotes element with ReasonCode "blanco" is missing
113    #[error("The RejectedVotes element with ReasonCode 'blanco' is missing")]
114    MissingRejectedVotesBlank,
115
116    /// The RejectedVotes element with ReasonCode "ongeldig" is missing
117    #[error("The RejectedVotes element with ReasonCode 'ongeldig' is missing")]
118    MissingRejectedVotesInvalid,
119
120    /// A Selection is missing a selection type (i.e. Candidate, AffiliationIdentifier or ReferendumOptionIdentifier)
121    #[error(
122        "A Selection is missing a selection type (i.e. Candidate, AffiliationIdentifier or ReferendumOptionIdentifier)"
123    )]
124    MissingSelectionType,
125
126    /// A field that is required for building a struct is missing.
127    #[error("A required property '{0}' is missing for building this struct")]
128    MissingBuildProperty(&'static str),
129
130    /// The NominationDate is before the ElectionDate, which is not allowed.
131    #[error("The NominationDate is before the ElectionDate, which is not allowed")]
132    NominationDateNotBeforeElectionDate,
133
134    /// The ElectionSubcategory is not valid for the ElectionCategory.
135    #[error("The ElectionSubcategory is not valid for the ElectionCategory")]
136    InvalidElectionSubcategory,
137
138    /// The voting method specified in the document is not supported.
139    #[error("The voting method specified in the document is not supported, only SPV is supported")]
140    UnsupportedVotingMethod,
141
142    /// The preference threshold specified in the document is not valid.
143    #[error("The preference threshold specified does not match the election identifier")]
144    InvalidPreferenceThreshold,
145
146    /// The number of seats specified in the document is not valid.
147    #[error("The number of seats specified does not match the subcategory")]
148    InvalidNumberOfSeats,
149
150    /// A referendum option was found where none was expected.
151    #[error("A referendum option was found where none was expected")]
152    UnexpectedReferendumOptionSelection,
153
154    /// A candidate was found without an affiliation, which is not allowed.
155    #[error("A candidate without affiliation was found")]
156    CandidateWithoutAffiliationFound,
157
158    /// Missing a Contest element when at least one was expected.
159    #[error("Missing a Contest element when at least one was expected")]
160    MissingContest,
161
162    /// Missing the TotalVotes element (while creating CSV).
163    #[error("Missing the TotalVotes element")]
164    MissingTotalVotes,
165
166    /// Could not find a candidate for the given affiliation and candidate ids.
167    #[error("Could not find a candidate for affiliation id {0} and candidate id {1}")]
168    UnknownCandidate(AffiliationId, CandidateId),
169
170    /// Could not find an affiliation for the given affiliation id.
171    #[error("Could not find an affiliation for affiliation id {0}")]
172    UnknownAffiliation(AffiliationId),
173
174    /// A custom error with something that can be displayed
175    #[error("Custom error: {0}")]
176    Custom(Box<dyn CustomError>),
177}
178
179/// Custom error type that can be used in EMLErrorKind::Custom
180pub trait CustomError: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static {}
181
182impl<T> CustomError for T where T: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static {}
183
184impl EMLErrorKind {
185    /// Adds span information to the error.
186    pub(crate) fn with_span(self, span: Span) -> EMLError {
187        EMLError::Positioned { kind: self, span }
188    }
189
190    /// Converts the error kind to an error without span information.
191    pub(crate) fn without_span(self) -> EMLError {
192        EMLError::UnknownPosition { kind: self }
193    }
194}
195
196/// An error encountered during EML_NL processing.
197///
198/// The error includes the kind of error as well as an optional span indicating
199/// where in the source XML the error approximately occured.
200#[derive(thiserror::Error, Debug)]
201pub enum EMLError {
202    /// An error with position information in a document
203    #[error("Error in EML: {kind} at position {span:?}")]
204    Positioned {
205        /// The kind of error that occured
206        kind: EMLErrorKind,
207        /// The span (position) in the document where the error occured
208        span: Span,
209    },
210    /// An error without position information
211    #[error("Error in EML: {kind}")]
212    UnknownPosition {
213        /// The kind of error that occured
214        kind: EMLErrorKind,
215    },
216    /// A list of multiple errors
217    #[error("Multiple errors in EML: {0}")]
218    Multiple(MultipleEMLErrors),
219}
220
221/// An error containing multiple EMLErrors
222#[derive(Debug)]
223pub struct MultipleEMLErrors {
224    /// The list of errors
225    pub errors: Vec<EMLError>,
226}
227
228impl std::fmt::Display for MultipleEMLErrors {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        writeln!(
231            f,
232            "{} non-fatal error(s) and then: {}",
233            self.errors.len() - 1,
234            self.errors.last().unwrap()
235        )
236    }
237}
238
239impl EMLError {
240    /// Create a new invalid value error
241    pub(crate) fn invalid_value(
242        field: OwnedQualifiedName,
243        source: impl std::error::Error + Send + Sync + 'static,
244        span: Option<Span>,
245    ) -> Self {
246        let kind = EMLErrorKind::InvalidValue(field, Box::new(source));
247        if let Some(span) = span {
248            EMLError::Positioned { kind, span }
249        } else {
250            EMLError::UnknownPosition { kind }
251        }
252    }
253
254    /// Create a new custom error.
255    pub fn custom(source: impl CustomError) -> Self {
256        EMLErrorKind::Custom(Box::new(source)).without_span()
257    }
258
259    /// Create an EMLError from a list of errors.
260    pub(crate) fn from_vec(errors: Vec<EMLError>) -> Self {
261        if errors.len() == 1 {
262            errors
263                .into_iter()
264                .next()
265                .expect("Vec must have one element")
266        } else {
267            EMLError::Multiple(MultipleEMLErrors { errors })
268        }
269    }
270
271    /// Create an EMLError from a vector of errors.
272    pub(crate) fn from_vec_with_additional(mut errors: Vec<EMLError>, error: EMLError) -> Self {
273        errors.push(error);
274        Self::from_vec(errors)
275    }
276
277    /// Returns the kind of this error.
278    ///
279    /// When this error consists of multiple errors, None is returned.
280    ///
281    /// Note: when multiple errors are present, the kind of the last error is returned.
282    pub fn kind(&self) -> &EMLErrorKind {
283        match self {
284            EMLError::Positioned { kind, .. } => kind,
285            EMLError::UnknownPosition { kind } => kind,
286            EMLError::Multiple(MultipleEMLErrors { errors }) => errors
287                .last()
288                .map(|e| e.kind())
289                .expect("Errors vec cannot be empty"),
290        }
291    }
292
293    /// Returns the kind of this error as an EMLErrorKind, consuming the error.
294    ///
295    /// When this error consists of multiple errors, the kind of the last error is returned.
296    pub fn into_kind(self) -> EMLErrorKind {
297        match self {
298            EMLError::Positioned { kind, .. } => kind,
299            EMLError::UnknownPosition { kind } => kind,
300            EMLError::Multiple(MultipleEMLErrors { errors }) => errors
301                .into_iter()
302                .last()
303                .map(|e| e.into_kind())
304                .expect("Errors vec cannot be empty"),
305        }
306    }
307
308    /// Returns the span of this error, if available.
309    ///
310    /// Note: when multiple errors are present, the span of the last error is returned.
311    pub fn span(&self) -> Option<Span> {
312        match self {
313            EMLError::Positioned { span, .. } => Some(*span),
314            EMLError::UnknownPosition { .. } => None,
315            EMLError::Multiple(MultipleEMLErrors { errors }) => {
316                errors.last().and_then(|e| e.span())
317            }
318        }
319    }
320}
321
322/// Extension trait for Result to add context to EMLError
323pub(crate) trait EMLResultExt<T> {
324    /// Adds span information to the error if it occurs.
325    fn with_span(self, span: Span) -> Result<T, EMLError>;
326    /// Converts the error kind to an error without span information.
327    fn without_span(self) -> Result<T, EMLError>;
328}
329
330impl<T, I> EMLResultExt<T> for Result<T, I>
331where
332    I: Into<EMLErrorKind>,
333{
334    fn with_span(self, span: Span) -> Result<T, EMLError> {
335        self.map_err(|kind| EMLError::Positioned {
336            kind: kind.into(),
337            span,
338        })
339    }
340
341    fn without_span(self) -> Result<T, EMLError> {
342        self.map_err(|kind| EMLError::UnknownPosition { kind: kind.into() })
343    }
344}
345
346/// Extension trait for Result to add context to EMLError for errors that can be
347/// converted into EMLErrorKind::InvalidValue.
348pub(crate) trait EMLValueResultExt<T> {
349    /// Convert the error into an EMLError with context about the field that caused the error.
350    fn wrap_field_value_error(
351        self,
352        element_name: impl Into<OwnedQualifiedName>,
353    ) -> Result<T, EMLError>;
354
355    /// Convert the error into an EMLError that the value is invalid.
356    fn wrap_value_error(self) -> Result<T, EMLError>;
357}
358
359impl<T, I> EMLValueResultExt<T> for Result<T, I>
360where
361    I: std::error::Error + Send + Sync + 'static,
362{
363    fn wrap_field_value_error(
364        self,
365        element_name: impl Into<OwnedQualifiedName>,
366    ) -> Result<T, EMLError> {
367        self.map_err(|e| EMLError::invalid_value(element_name.into(), Box::new(e), None))
368    }
369
370    fn wrap_value_error(self) -> Result<T, EMLError> {
371        self.map_err(|e| EMLErrorKind::ValueConversionError(Box::new(e)).without_span())
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::NS_EML;
379
380    #[test]
381    fn test_creating_invalid_value_error() {
382        let error = EMLError::invalid_value(
383            OwnedQualifiedName::from_static("Test", Some(NS_EML)),
384            std::io::Error::other("error"),
385            None,
386        );
387
388        assert!(matches!(
389            error,
390            EMLError::UnknownPosition {
391                kind: EMLErrorKind::InvalidValue(_, _)
392            }
393        ));
394
395        let error_with_span = EMLError::invalid_value(
396            OwnedQualifiedName::from_static("Test", Some(NS_EML)),
397            std::io::Error::other("error"),
398            Some(Span { start: 10, end: 20 }),
399        );
400
401        assert!(matches!(
402            error_with_span,
403            EMLError::Positioned {
404                kind: EMLErrorKind::InvalidValue(_, _),
405                span: Span { start: 10, end: 20 }
406            }
407        ));
408    }
409
410    #[test]
411    fn test_creating_multiple_errors() {
412        let err1 = EMLErrorKind::UnexpectedEndElement.with_span(Span { start: 0, end: 5 });
413        let err2 =
414            EMLErrorKind::MissingElement(OwnedQualifiedName::from_static("Test", Some(NS_EML)))
415                .with_span(Span { start: 10, end: 15 });
416
417        let multiple_error = EMLError::from_vec_with_additional(vec![err1], err2);
418        assert!(matches!(multiple_error, EMLError::Multiple(_)));
419
420        let err3 = EMLErrorKind::UnexpectedEof.with_span(Span { start: 0, end: 10 });
421        let multiple_error2 = EMLError::from_vec_with_additional(vec![], err3);
422        assert!(matches!(
423            multiple_error2,
424            EMLError::Positioned {
425                kind: EMLErrorKind::UnexpectedEof,
426                span: Span { start: 0, end: 10 }
427            }
428        ));
429    }
430
431    #[test]
432    fn get_data_from_error() {
433        let err = EMLErrorKind::UnexpectedEof.with_span(Span { start: 0, end: 10 });
434        assert!(matches!(err.kind(), &EMLErrorKind::UnexpectedEof));
435        assert_eq!(err.span(), Some(Span { start: 0, end: 10 }));
436
437        let err2 = EMLError::UnknownPosition {
438            kind: EMLErrorKind::UnexpectedElement(
439                OwnedQualifiedName::from_static("Test", None),
440                OwnedQualifiedName::from_static("Test", None),
441            ),
442        };
443
444        assert!(matches!(
445            err2.kind(),
446            &EMLErrorKind::UnexpectedElement(_, _)
447        ));
448        assert_eq!(err2.span(), None);
449
450        let err3 = EMLError::Multiple(MultipleEMLErrors {
451            errors: vec![
452                EMLError::Positioned {
453                    kind: EMLErrorKind::UnexpectedElement(
454                        OwnedQualifiedName::from_static("Test", None),
455                        OwnedQualifiedName::from_static("Test", None),
456                    ),
457                    span: Span { start: 0, end: 5 },
458                },
459                EMLError::UnknownPosition {
460                    kind: EMLErrorKind::MissingElement(OwnedQualifiedName::from_static(
461                        "Test", None,
462                    )),
463                },
464            ],
465        });
466        assert!(matches!(err3.kind(), &EMLErrorKind::MissingElement(_)));
467        assert_eq!(err3.span(), None);
468    }
469}