Skip to main content

ccsds_ndm/
error.rs

1// SPDX-FileCopyrightText: 2025 Jochim Maene <jochim.maene+github@gmail.com>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use crate::types::EpochError;
6use std::borrow::Cow;
7use thiserror::Error;
8use winnow::error::{AddContext, ParserError, StrContext};
9use winnow::stream::Stream;
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct ParseDiagnostic {
13    pub line: usize,
14    pub column: usize,
15    pub message: String,
16    pub contexts: Vec<&'static str>,
17}
18
19#[derive(Debug, Clone, PartialEq, Default)]
20pub struct RawParsePosition {
21    pub offset: usize,
22    pub message: Cow<'static, str>,
23    pub contexts: ContextStack,
24}
25
26impl RawParsePosition {
27    pub fn into_parse_error(self, input: &str) -> KvnParseError {
28        // Use 0 as default if we don't have location info yet,
29        // to_ccsds_error + with_location will fix this.
30        // Actually, with_location recalculates from offset.
31        // But ParseDiagnostic computes everything.
32        let diag = ParseDiagnostic::new(input, self.offset, &*self.message);
33        KvnParseError {
34            line: diag.line,
35            column: diag.column,
36            message: self.message.into_owned(),
37            contexts: self.contexts.to_vec(),
38            offset: self.offset,
39        }
40    }
41}
42
43impl ParseDiagnostic {
44    /// Creates a new diagnostic from an input string and byte offset.
45    pub fn new(input: &str, offset: usize, message: impl Into<String>) -> Self {
46        let offset = offset.min(input.len());
47        let prefix = &input[..offset];
48        let line = prefix.as_bytes().iter().filter(|&&b| b == b'\n').count() + 1;
49        let line_start = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
50        let column = prefix[line_start..].chars().count();
51
52        Self {
53            line,
54            column: column + 1,
55            message: message.into(),
56            contexts: Vec::new(),
57        }
58    }
59
60    /// Adds contexts to the diagnostic.
61    pub fn with_contexts(mut self, contexts: Vec<&'static str>) -> Self {
62        self.contexts = contexts;
63        self
64    }
65}
66
67/// Detailed error information for KVN parsing failures.
68#[derive(Debug, Clone, PartialEq, Error)]
69pub struct KvnParseError {
70    pub line: usize,
71    pub column: usize,
72    pub message: String,
73    pub contexts: Vec<&'static str>,
74    pub offset: usize, // Track raw offset for lazy location calculation
75}
76
77impl std::fmt::Display for KvnParseError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(
80            f,
81            "KVN parsing error at line {}, column {}: {}",
82            self.line, self.column, self.message
83        )?;
84        if !self.contexts.is_empty() {
85            write!(f, "\nContext: {}", self.contexts.join(" > "))?;
86        }
87        Ok(())
88    }
89}
90
91/// Lightweight error for enum string conversion.
92#[derive(Debug, Clone, PartialEq, Error)]
93#[error("Invalid value '{value}' for field '{field}'; expected one of: {expected}")]
94pub struct EnumParseError {
95    pub field: &'static str,
96    pub value: String,
97    pub expected: &'static str,
98}
99
100/// Errors related to the physical format or syntax of the NDM.
101#[derive(Debug, Error)]
102#[non_exhaustive]
103pub enum FormatError {
104    /// Errors occurring during KVN parsing.
105    #[error(transparent)]
106    Kvn(#[from] Box<KvnParseError>),
107
108    /// Errors occurring during XML parsing.
109    #[error("XML error: {0}")]
110    Xml(
111        #[source]
112        #[from]
113        quick_xml::Error,
114    ),
115
116    /// Errors occurring during XML deserialization.
117    #[error("XML deserialization error: {0}")]
118    XmlDe(
119        #[source]
120        #[from]
121        quick_xml::DeError,
122    ),
123
124    /// Errors occurring during XML serialization.
125    #[error("XML serialization error: {0}")]
126    XmlSer(
127        #[source]
128        #[from]
129        quick_xml::se::SeError,
130    ),
131
132    /// Error when parsing a floating point number fails.
133    #[error("Parse float error: {0}")]
134    ParseFloat(
135        #[source]
136        #[from]
137        std::num::ParseFloatError,
138    ),
139
140    /// Error when parsing an integer number fails.
141    #[error("Parse int error: {0}")]
142    ParseInt(
143        #[source]
144        #[from]
145        std::num::ParseIntError,
146    ),
147
148    /// Error during enum parsing.
149    #[error(transparent)]
150    Enum(#[from] EnumParseError),
151
152    /// Error when the format of a value or segment is invalid.
153    #[error("Invalid format: {0}")]
154    InvalidFormat(String),
155
156    /// Errors occurring during XML deserialization with added context.
157    #[error("{context}: {source}")]
158    XmlWithContext {
159        context: String,
160        #[source]
161        source: quick_xml::DeError,
162    },
163}
164
165/// Errors related to the validation of NDM data against CCSDS rules.
166#[derive(Debug, Clone, PartialEq, Error)]
167#[non_exhaustive]
168pub enum ValidationError {
169    /// A required field was missing in the message.
170    #[error("Missing required field: {field} in block {block}")]
171    MissingRequiredField {
172        block: Cow<'static, str>,
173        field: Cow<'static, str>,
174        line: Option<usize>,
175    },
176
177    /// Two or more fields are in conflict.
178    #[error("Conflicting fields: {fields:?}")]
179    Conflict {
180        fields: Vec<Cow<'static, str>>,
181        line: Option<usize>,
182    },
183
184    /// A value was provided that does not match the CCSDS specification.
185    #[error("Invalid value for '{field}': '{value}' (expected {expected})")]
186    InvalidValue {
187        field: Cow<'static, str>,
188        value: String,
189        expected: Cow<'static, str>,
190        line: Option<usize>,
191    },
192
193    /// Specific validation error for values out of expected range.
194    #[error("Value for '{name}' is out of range: {value} (expected {expected})")]
195    OutOfRange {
196        name: Cow<'static, str>,
197        value: String,
198        expected: Cow<'static, str>,
199        line: Option<usize>,
200    },
201
202    /// General validation errors for cases not covered by specific variants.
203    #[error("Validation error: {message}")]
204    Generic {
205        message: Cow<'static, str>,
206        line: Option<usize>,
207    },
208}
209
210impl ValidationError {
211    /// Convenience constructor for a missing required field error.
212    pub fn missing_required(
213        block: impl Into<Cow<'static, str>>,
214        field: impl Into<Cow<'static, str>>,
215    ) -> Self {
216        Self::MissingRequiredField {
217            block: block.into(),
218            field: field.into(),
219            line: None,
220        }
221    }
222
223    /// Convenience constructor for an invalid value error.
224    pub fn invalid_value(
225        field: impl Into<Cow<'static, str>>,
226        value: impl Into<String>,
227        expected: impl Into<Cow<'static, str>>,
228    ) -> Self {
229        Self::InvalidValue {
230            field: field.into(),
231            value: value.into(),
232            expected: expected.into(),
233            line: None,
234        }
235    }
236
237    /// Convenience constructor for a generic validation error.
238    pub fn generic(message: impl Into<Cow<'static, str>>) -> Self {
239        Self::Generic {
240            message: message.into(),
241            line: None,
242        }
243    }
244
245    /// Convenience constructor for conflicting fields.
246    pub fn conflict<I>(fields: I) -> Self
247    where
248        I: IntoIterator<Item = Cow<'static, str>>,
249    {
250        Self::Conflict {
251            fields: fields.into_iter().collect(),
252            line: None,
253        }
254    }
255}
256
257/// Trait for errors that can be enriched with line/column info.
258pub trait WithLocation: Sized {
259    /// Adds location information to the error.
260    fn with_line(self, line: usize) -> Self;
261}
262
263impl WithLocation for ValidationError {
264    fn with_line(mut self, line: usize) -> Self {
265        match &mut self {
266            ValidationError::OutOfRange {
267                line: ref mut l, ..
268            }
269            | ValidationError::InvalidValue {
270                line: ref mut l, ..
271            }
272            | ValidationError::MissingRequiredField {
273                line: ref mut l, ..
274            }
275            | ValidationError::Conflict {
276                line: ref mut l, ..
277            }
278            | ValidationError::Generic {
279                line: ref mut l, ..
280            } => {
281                if l.is_none() {
282                    *l = Some(line);
283                }
284            }
285        }
286        self
287    }
288}
289
290/// The top-level error type for the CCSDS NDM library.
291///
292/// This enum wraps all possible errors that can occur during NDM parsing,
293/// validation, serialization, and I/O.
294///
295/// # Example: Handling Parse Errors
296/// ```no_run
297/// use ccsds_ndm::messages::opm::Opm;
298/// use ccsds_ndm::error::CcsdsNdmError;
299/// use ccsds_ndm::kvn::parser::ParseKvn;
300///
301/// match Opm::from_kvn_str("CCSDS_OPM_VERS = 3.0\n...") {
302///     Ok(opm) => println!("Parsed: {:?}", opm),
303///     Err(e) => {
304///         if let Some(enum_err) = e.as_enum_error() {
305///             eprintln!("Invalid enum value '{}' for field '{}'", enum_err.value, enum_err.field);
306///         } else if let Some(validation_err) = e.as_validation_error() {
307///             eprintln!("Validation error: {}", validation_err);
308///         } else {
309///             eprintln!("Error: {}", e);
310///         }
311///     }
312/// }
313/// ```
314#[derive(Error, Debug)]
315#[non_exhaustive]
316pub enum CcsdsNdmError {
317    /// Errors occurring during I/O operations.
318    #[error("I/O error: {0}")]
319    Io(
320        #[source]
321        #[from]
322        std::io::Error,
323    ),
324
325    /// Errors related to NDM format or syntax.
326    #[error(transparent)]
327    Format(#[from] Box<FormatError>),
328
329    /// Errors related to NDM data validation.
330    #[error(transparent)]
331    Validation(#[from] Box<ValidationError>),
332
333    /// Errors related to CCSDS Epochs.
334    #[error("Epoch error: {0}")]
335    Epoch(
336        #[source]
337        #[from]
338        EpochError,
339    ),
340
341    /// Error for unsupported CCSDS message types.
342    #[error("Unsupported message type: {0}")]
343    UnsupportedMessage(String),
344
345    /// Error when an unexpected end of input is reached.
346    #[error("Unexpected end of input: {context}")]
347    UnexpectedEof { context: String },
348}
349
350/// A stack-allocated collection of error contexts.
351///
352/// **Note**: Capacity is limited to 3 contexts. Additional contexts are silently ignored.
353/// This is a deliberate trade-off for performance in the hot parsing path.
354#[derive(Debug, Clone, PartialEq, Default)]
355pub struct ContextStack {
356    contexts: [&'static str; 3],
357    len: usize,
358}
359
360impl ContextStack {
361    pub fn new() -> Self {
362        Self::default()
363    }
364
365    pub fn push(&mut self, context: &'static str) {
366        if self.len < self.contexts.len() {
367            self.contexts[self.len] = context;
368            self.len += 1;
369        }
370    }
371
372    pub fn last(&self) -> Option<&&'static str> {
373        if self.len > 0 {
374            Some(&self.contexts[self.len - 1])
375        } else {
376            None
377        }
378    }
379
380    pub fn to_vec(&self) -> Vec<&'static str> {
381        self.contexts[..self.len].to_vec()
382    }
383}
384
385/// A lightweight internal error type for winnow parsers.
386#[derive(Debug, Clone, PartialEq)]
387pub struct InternalParserError {
388    pub message: Cow<'static, str>,
389    pub contexts: ContextStack,
390    pub kind: Box<ParserErrorKind>,
391}
392
393#[derive(Debug, Clone, PartialEq, Default)]
394#[non_exhaustive]
395pub enum ParserErrorKind {
396    #[default]
397    Kvn,
398    MissingRequiredField {
399        block: &'static str,
400        field: &'static str,
401    },
402    Validation(ValidationError),
403    Epoch(EpochError),
404    Enum(EnumParseError),
405    ParseInt(std::num::ParseIntError),
406    ParseFloat(std::num::ParseFloatError),
407}
408
409impl ParserError<&str> for InternalParserError {
410    type Inner = ();
411    fn from_input(_input: &&str) -> Self {
412        Self {
413            message: Cow::Borrowed(""),
414            contexts: ContextStack::new(),
415            kind: Box::new(ParserErrorKind::default()),
416        }
417    }
418
419    fn into_inner(self) -> std::result::Result<Self::Inner, Self> {
420        Ok(())
421    }
422}
423
424impl winnow::error::FromExternalError<&str, EpochError> for InternalParserError {
425    fn from_external_error(_input: &&str, e: EpochError) -> Self {
426        Self {
427            message: Cow::Borrowed(""),
428            contexts: ContextStack::new(),
429            kind: Box::new(ParserErrorKind::Epoch(e)),
430        }
431    }
432}
433
434impl winnow::error::FromExternalError<&str, std::num::ParseFloatError> for InternalParserError {
435    fn from_external_error(_input: &&str, e: std::num::ParseFloatError) -> Self {
436        Self {
437            message: Cow::Borrowed(""),
438            contexts: ContextStack::new(),
439            kind: Box::new(ParserErrorKind::ParseFloat(e)),
440        }
441    }
442}
443
444impl winnow::error::FromExternalError<&str, std::num::ParseIntError> for InternalParserError {
445    fn from_external_error(_input: &&str, e: std::num::ParseIntError) -> Self {
446        Self {
447            message: Cow::Borrowed(""),
448            contexts: ContextStack::new(),
449            kind: Box::new(ParserErrorKind::ParseInt(e)),
450        }
451    }
452}
453
454impl winnow::error::FromExternalError<&str, EnumParseError> for InternalParserError {
455    fn from_external_error(_input: &&str, e: EnumParseError) -> Self {
456        Self {
457            message: Cow::Borrowed(""),
458            contexts: ContextStack::new(),
459            kind: Box::new(ParserErrorKind::Enum(e)),
460        }
461    }
462}
463
464impl winnow::error::FromExternalError<&str, ValidationError> for InternalParserError {
465    fn from_external_error(_input: &&str, e: ValidationError) -> Self {
466        Self {
467            message: Cow::Borrowed(""),
468            contexts: ContextStack::new(),
469            kind: Box::new(ParserErrorKind::Validation(e)),
470        }
471    }
472}
473
474impl winnow::error::FromExternalError<&str, CcsdsNdmError> for InternalParserError {
475    fn from_external_error(input: &&str, e: CcsdsNdmError) -> Self {
476        match e {
477            CcsdsNdmError::Validation(ve) => Self::from_external_error(input, *ve),
478            CcsdsNdmError::Epoch(ee) => Self::from_external_error(input, ee),
479            CcsdsNdmError::Format(fe) => match *fe {
480                FormatError::Enum(ee) => Self::from_external_error(input, ee),
481                FormatError::ParseFloat(pfe) => Self::from_external_error(input, pfe),
482                FormatError::ParseInt(pie) => Self::from_external_error(input, pie),
483                _ => Self {
484                    message: Cow::Owned(fe.to_string()),
485                    contexts: ContextStack::new(),
486                    kind: Box::new(ParserErrorKind::default()),
487                },
488            },
489            _ => Self {
490                message: Cow::Owned(e.to_string()),
491                contexts: ContextStack::new(),
492                kind: Box::new(ParserErrorKind::default()),
493            },
494        }
495    }
496}
497
498impl AddContext<&str, StrContext> for InternalParserError {
499    fn add_context(
500        mut self,
501        _input: &&str,
502        _token: &<&str as Stream>::Checkpoint,
503        context: StrContext,
504    ) -> Self {
505        match context {
506            StrContext::Label(l) => {
507                if self.contexts.last() != Some(&l) {
508                    self.contexts.push(l);
509                }
510            }
511            StrContext::Expected(e) => {
512                self.message = Cow::Owned(format!("Expected {}", e));
513            }
514            _ => {} // Ignore other context types for now
515        }
516        self
517    }
518}
519
520impl From<ValidationError> for CcsdsNdmError {
521    fn from(e: ValidationError) -> Self {
522        CcsdsNdmError::Validation(Box::new(e))
523    }
524}
525
526impl From<FormatError> for CcsdsNdmError {
527    fn from(e: FormatError) -> Self {
528        CcsdsNdmError::Format(Box::new(e))
529    }
530}
531
532impl From<EnumParseError> for CcsdsNdmError {
533    fn from(e: EnumParseError) -> Self {
534        CcsdsNdmError::Format(Box::new(FormatError::Enum(e)))
535    }
536}
537
538impl From<std::num::ParseFloatError> for CcsdsNdmError {
539    fn from(e: std::num::ParseFloatError) -> Self {
540        CcsdsNdmError::Format(Box::new(FormatError::ParseFloat(e)))
541    }
542}
543
544impl From<std::num::ParseIntError> for CcsdsNdmError {
545    fn from(e: std::num::ParseIntError) -> Self {
546        CcsdsNdmError::Format(Box::new(FormatError::ParseInt(e)))
547    }
548}
549
550impl From<quick_xml::DeError> for CcsdsNdmError {
551    fn from(e: quick_xml::DeError) -> Self {
552        CcsdsNdmError::Format(Box::new(FormatError::XmlDe(e)))
553    }
554}
555
556impl From<quick_xml::se::SeError> for CcsdsNdmError {
557    fn from(e: quick_xml::se::SeError) -> Self {
558        CcsdsNdmError::Format(Box::new(FormatError::XmlSer(e)))
559    }
560}
561
562impl From<quick_xml::Error> for CcsdsNdmError {
563    fn from(e: quick_xml::Error) -> Self {
564        CcsdsNdmError::Format(Box::new(FormatError::Xml(e)))
565    }
566}
567
568impl winnow::error::FromExternalError<&str, EpochError> for CcsdsNdmError {
569    fn from_external_error(_input: &&str, e: EpochError) -> Self {
570        CcsdsNdmError::Epoch(e)
571    }
572}
573
574impl winnow::error::FromExternalError<&str, std::num::ParseFloatError> for CcsdsNdmError {
575    fn from_external_error(_input: &&str, e: std::num::ParseFloatError) -> Self {
576        CcsdsNdmError::Format(Box::new(FormatError::ParseFloat(e)))
577    }
578}
579
580impl winnow::error::FromExternalError<&str, std::num::ParseIntError> for CcsdsNdmError {
581    fn from_external_error(_input: &&str, e: std::num::ParseIntError) -> Self {
582        CcsdsNdmError::Format(Box::new(FormatError::ParseInt(e)))
583    }
584}
585
586impl AddContext<&str, StrContext> for CcsdsNdmError {
587    fn add_context(
588        mut self,
589        _input: &&str,
590        _token: &<&str as Stream>::Checkpoint,
591        context: StrContext,
592    ) -> Self {
593        if let CcsdsNdmError::Format(ref mut format_err) = self {
594            if let FormatError::Kvn(ref mut inner) = **format_err {
595                match context {
596                    StrContext::Label(l) => {
597                        if inner.contexts.last() != Some(&l) {
598                            inner.contexts.push(l);
599                        }
600                    }
601                    StrContext::Expected(e) => inner.message = format!("Expected {}", e),
602                    _ => {} // Ignore other context types for now
603                }
604            }
605        }
606        self
607    }
608}
609
610impl CcsdsNdmError {
611    /// Returns the inner KVN parse error if this is a FormatError::Kvn.
612    pub fn as_kvn_parse_error(&self) -> Option<&KvnParseError> {
613        match self {
614            CcsdsNdmError::Format(e) => match **e {
615                FormatError::Kvn(ref err) => Some(err),
616                _ => None,
617            },
618            _ => None,
619        }
620    }
621
622    /// Returns the inner validation error if this is a ValidationError.
623    pub fn as_validation_error(&self) -> Option<&ValidationError> {
624        match self {
625            CcsdsNdmError::Validation(e) => Some(e),
626            _ => None,
627        }
628    }
629
630    /// Returns the inner format error if this is a FormatError.
631    pub fn as_format_error(&self) -> Option<&FormatError> {
632        match self {
633            CcsdsNdmError::Format(e) => Some(e),
634            _ => None,
635        }
636    }
637
638    /// Returns the inner epoch error if this is an EpochError.
639    pub fn as_epoch_error(&self) -> Option<&EpochError> {
640        match self {
641            CcsdsNdmError::Epoch(e) => Some(e),
642            _ => None,
643        }
644    }
645
646    /// Returns the inner I/O error if this is an IoError.
647    pub fn as_io_error(&self) -> Option<&std::io::Error> {
648        match self {
649            CcsdsNdmError::Io(e) => Some(e),
650            _ => None,
651        }
652    }
653
654    /// Returns the inner XML error if this is an XmlError.
655    pub fn as_xml_error(&self) -> Option<&quick_xml::Error> {
656        match self {
657            CcsdsNdmError::Format(e) => match **e {
658                FormatError::Xml(ref xe) => Some(xe),
659                _ => None,
660            },
661            _ => None,
662        }
663    }
664
665    /// Returns true if this is any FormatError.
666    pub fn is_format_error(&self) -> bool {
667        matches!(self, CcsdsNdmError::Format(_))
668    }
669
670    /// Returns true if this is a KVN FormatError.
671    pub fn is_kvn_error(&self) -> bool {
672        self.as_kvn_parse_error().is_some()
673    }
674
675    /// Returns true if this is a ValidationError.
676    pub fn is_validation_error(&self) -> bool {
677        self.as_validation_error().is_some()
678    }
679
680    /// Returns true if this is an I/O error.
681    pub fn is_io_error(&self) -> bool {
682        matches!(self, CcsdsNdmError::Io(_))
683    }
684
685    /// Returns true if this is an epoch error.
686    pub fn is_epoch_error(&self) -> bool {
687        matches!(self, CcsdsNdmError::Epoch(_))
688    }
689
690    /// Returns the inner EnumParseError if this is a FormatError::Enum.
691    pub fn as_enum_error(&self) -> Option<&EnumParseError> {
692        match self {
693            CcsdsNdmError::Format(e) => match **e {
694                FormatError::Enum(ref ee) => Some(ee),
695                _ => None,
696            },
697            _ => None,
698        }
699    }
700
701    /// Returns the inner ParseIntError if this is a FormatError::ParseInt.
702    pub fn as_parse_int_error(&self) -> Option<&std::num::ParseIntError> {
703        match self {
704            CcsdsNdmError::Format(e) => match **e {
705                FormatError::ParseInt(ref pie) => Some(pie),
706                _ => None,
707            },
708            _ => None,
709        }
710    }
711
712    /// Returns the inner ParseFloatError if this is a FormatError::ParseFloat.
713    pub fn as_parse_float_error(&self) -> Option<&std::num::ParseFloatError> {
714        match self {
715            CcsdsNdmError::Format(e) => match **e {
716                FormatError::ParseFloat(ref pfe) => Some(pfe),
717                _ => None,
718            },
719            _ => None,
720        }
721    }
722
723    /// Populates location information for variants with line info.
724    pub fn with_location(mut self, input: &str, offset: usize) -> Self {
725        match self {
726            CcsdsNdmError::Format(ref mut format_err) => {
727                if let FormatError::Kvn(ref mut inner) = **format_err {
728                    // Avoid re-calculating if already populated via RawParsePosition
729                    if inner.line > 0 {
730                        return self;
731                    }
732
733                    let target_offset = if offset > 0 {
734                        offset
735                    } else if inner.offset > 0 {
736                        inner.offset
737                    } else {
738                        0
739                    };
740
741                    let diag = ParseDiagnostic::new(input, target_offset, "");
742                    inner.line = diag.line;
743                    inner.column = diag.column;
744                    inner.offset = target_offset;
745                }
746            }
747            CcsdsNdmError::Validation(ref mut val_err) => match **val_err {
748                ValidationError::InvalidValue { ref mut line, .. }
749                | ValidationError::MissingRequiredField { ref mut line, .. }
750                | ValidationError::Conflict { ref mut line, .. }
751                | ValidationError::Generic { ref mut line, .. }
752                | ValidationError::OutOfRange { ref mut line, .. } => {
753                    if line.is_none() {
754                        let diag = ParseDiagnostic::new(input, offset, "");
755                        *line = Some(diag.line);
756                    }
757                }
758            },
759            _ => {} // Other variants don't have location info
760        }
761        self
762    }
763}
764
765pub type Result<T> = std::result::Result<T, CcsdsNdmError>;
766
767#[cfg(test)]
768mod tests {
769    use super::*;
770
771    #[test]
772    fn test_kvn_parse_error_display() {
773        let err = KvnParseError {
774            line: 10,
775            column: 5,
776            message: "Test error".into(),
777            contexts: vec!["Header", "Version"],
778            offset: 100,
779        };
780        let s = format!("{}", err);
781        assert!(s.contains("line 10, column 5"));
782        assert!(s.contains("Test error"));
783        assert!(s.contains("Header > Version"));
784    }
785
786    #[test]
787    fn test_enum_parse_error_display() {
788        let err = EnumParseError {
789            field: "FIELD",
790            value: "VAL".into(),
791            expected: "A or B",
792        };
793        let s = format!("{}", err); // uses default error display because of `thiserror`
794                                    // We defined #[error("Invalid value '{value}' for field '{field}'; expected one of: {expected}")]
795        assert!(s.contains("Invalid value 'VAL' for field 'FIELD'"));
796        assert!(s.contains("expected one of: A or B"));
797    }
798
799    #[test]
800    fn test_validation_error_display() {
801        let err = ValidationError::MissingRequiredField {
802            block: "BLOCK".into(),
803            field: "FIELD".into(),
804            line: Some(42),
805        };
806        let s = format!("{}", err);
807        assert!(s.contains("Missing required field: FIELD in block BLOCK"));
808    }
809
810    #[test]
811    fn test_validation_error_with_location() {
812        let mut err = ValidationError::OutOfRange {
813            name: "N".into(),
814            value: "V".into(),
815            expected: "E".into(),
816            line: None,
817        };
818        // Should set line
819        err = err.with_line(123);
820        if let ValidationError::OutOfRange { line, .. } = err {
821            assert_eq!(line, Some(123));
822        } else {
823            panic!("Wrong variant");
824        }
825
826        // Should NOT overwrite line if already set
827        err = err.with_line(456);
828        if let ValidationError::OutOfRange { line, .. } = err {
829            assert_eq!(line, Some(123));
830        } else {
831            panic!("Wrong variant");
832        }
833    }
834
835    #[test]
836    fn test_ccsds_ndm_error_helpers() {
837        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "io");
838        let err: CcsdsNdmError = io_err.into();
839        assert!(err.as_io_error().is_some());
840        assert!(err.is_io_error());
841        assert_eq!(format!("{}", err), "I/O error: io");
842
843        let val_err = ValidationError::Generic {
844            message: "g".into(),
845            line: None,
846        };
847        let err: CcsdsNdmError = val_err.into();
848        assert!(err.as_validation_error().is_some());
849        assert!(err.is_validation_error());
850
851        let fmt_err = FormatError::InvalidFormat("f".into());
852        let err: CcsdsNdmError = fmt_err.into();
853        assert!(err.as_format_error().is_some());
854        assert!(err.is_format_error());
855        assert!(!err.is_kvn_error());
856
857        let enum_err = EnumParseError {
858            field: "F",
859            value: "V".into(),
860            expected: "E",
861        };
862        let err: CcsdsNdmError = enum_err.into();
863        assert!(err.as_enum_error().is_some());
864
865        let pfe_err = "abc".parse::<f64>().unwrap_err();
866        let err: CcsdsNdmError = pfe_err.into();
867        assert!(err.as_parse_float_error().is_some());
868
869        let pie_err = "abc".parse::<i32>().unwrap_err();
870        let err: CcsdsNdmError = pie_err.into();
871        assert!(err.as_parse_int_error().is_some());
872
873        let epoch_err = EpochError::InvalidFormat("2023".into());
874        let err: CcsdsNdmError = CcsdsNdmError::Epoch(epoch_err);
875        assert!(err.as_epoch_error().is_some());
876        assert!(err.is_epoch_error());
877
878        let eof_err = CcsdsNdmError::UnexpectedEof {
879            context: "ctx".into(),
880        };
881        assert_eq!(format!("{}", eof_err), "Unexpected end of input: ctx");
882
883        let unsupported = CcsdsNdmError::UnsupportedMessage("type".into());
884        assert_eq!(format!("{}", unsupported), "Unsupported message type: type");
885    }
886
887    #[test]
888    fn test_format_error_variants() {
889        let xml_err = quick_xml::Error::Io(std::sync::Arc::new(std::io::Error::new(
890            std::io::ErrorKind::Other,
891            "io",
892        )));
893        let err: CcsdsNdmError = FormatError::Xml(xml_err).into();
894        assert!(err.as_xml_error().is_some());
895
896        let fmt_err = FormatError::XmlWithContext {
897            context: "ctx".into(),
898            source: quick_xml::DeError::Custom("msg".into()),
899        };
900        assert!(format!("{}", fmt_err).contains("ctx"));
901    }
902
903    #[test]
904    fn test_with_location() {
905        let input = "LINE1\nLINE2\nLINE3";
906        let mut err = CcsdsNdmError::Validation(Box::new(ValidationError::Generic {
907            message: "msg".into(),
908            line: None,
909        }));
910        // Offset 6 is start of LINE2
911        err = err.with_location(input, 6);
912        if let CcsdsNdmError::Validation(ve) = err {
913            if let ValidationError::Generic { line, .. } = *ve {
914                assert_eq!(line, Some(2));
915            }
916        }
917
918        let kvn_err = KvnParseError {
919            line: 0,
920            column: 0,
921            message: "msg".into(),
922            contexts: vec![],
923            offset: 0,
924        };
925        let mut err = CcsdsNdmError::Format(Box::new(FormatError::Kvn(Box::new(kvn_err))));
926        err = err.with_location(input, 12); // start of LINE3
927        if let Some(ke) = err.as_kvn_parse_error() {
928            assert_eq!(ke.line, 3);
929        }
930    }
931
932    #[test]
933    fn test_context_stack() {
934        let mut stack = ContextStack::new();
935        assert_eq!(stack.last(), None);
936        stack.push("A");
937        stack.push("B");
938        stack.push("C");
939        assert_eq!(stack.last(), Some(&"C"));
940        stack.push("D"); // Ignored, capacity 3
941        assert_eq!(stack.last(), Some(&"C"));
942        assert_eq!(stack.to_vec(), vec!["A", "B", "C"]);
943    }
944
945    #[test]
946    fn test_internal_parser_error() {
947        use winnow::error::ParserError;
948        let input = "abc";
949        let mut err = InternalParserError::from_input(&input);
950        assert_eq!(err.message, "");
951        err.contexts.push("ctx");
952        assert_eq!(err.contexts.to_vec(), vec!["ctx"]);
953
954        // Test from_external_error for InternalParserError
955        use winnow::error::FromExternalError;
956        let enum_err = EnumParseError {
957            field: "F",
958            value: "V".into(),
959            expected: "E",
960        };
961        let err = InternalParserError::from_external_error(&input, enum_err);
962        assert!(matches!(*err.kind, ParserErrorKind::Enum(_)));
963
964        let ccsds_err = CcsdsNdmError::Validation(Box::new(ValidationError::Generic {
965            message: "m".into(),
966            line: None,
967        }));
968        let err = InternalParserError::from_external_error(&input, ccsds_err);
969        assert!(matches!(*err.kind, ParserErrorKind::Validation(_)));
970    }
971}