Skip to main content

redispatch_xml/validation/
mod.rs

1//! Structural and semantic validation for Redispatch 2.0 documents.
2//!
3//! ## Layers
4//!
5//! - **Structural** — verifies field constraints derivable from the XSD without
6//!   cross-field context (identifier lengths, version range, timestamp offsets,
7//!   participant ID format).
8//! - **Semantic** — cross-field rules from the BDEW AWT (e.g. an `ACO`
9//!   document must contain at least one `ActivationTimeSeries`).
10
11use crate::error::RedispatchXmlError;
12use crate::parse::Document;
13
14pub mod semantic;
15pub mod structural;
16
17// ── Validation result types ───────────────────────────────────────────────────
18
19/// A validation error (document is non-conformant and must not be processed).
20#[derive(Debug, Clone, PartialEq, thiserror::Error)]
21pub enum ValidationError {
22    #[error("document identifier must be 1–35 characters, got {0}")]
23    DocumentIdLength(usize),
24    #[error("document version must be 1–999, got {0}")]
25    DocumentVersionRange(u32),
26    #[error("market participant ID must be exactly 13 decimal digits, got {0:?}")]
27    MarketParticipantIdFormat(String),
28    #[error("timestamp must be UTC, got offset {0}")]
29    TimestampNotUtc(String),
30    #[error("time interval end must be after start")]
31    TimeIntervalOrder,
32    #[error("{0}")]
33    Structural(String),
34    #[error("{0}")]
35    Semantic(String),
36}
37
38/// A validation warning (non-fatal; document should still be processed).
39#[derive(Debug, Clone, PartialEq)]
40pub struct ValidationWarning(pub String);
41
42/// The combined result of validating a document.
43#[derive(Debug, Default, Clone)]
44pub struct ValidationResult {
45    /// Non-fatal warnings (processing may continue).
46    pub warnings: Vec<ValidationWarning>,
47    /// Validation errors (document is non-conformant).
48    pub errors: Vec<ValidationError>,
49}
50
51impl ValidationResult {
52    /// Return `true` if there are no validation errors.
53    pub fn is_valid(&self) -> bool {
54        self.errors.is_empty()
55    }
56
57    /// Convert to a [`Result`], returning the first error on failure.
58    ///
59    /// If you need **all** errors, use [`Self::into_errors`] instead.
60    pub fn into_result(mut self) -> Result<Vec<ValidationWarning>, ValidationError> {
61        if self.errors.is_empty() {
62            Ok(self.warnings)
63        } else {
64            Err(self.errors.remove(0))
65        }
66    }
67
68    /// Convert to a [`Result`], returning **all** validation errors on failure.
69    ///
70    /// Prefer this over [`Self::into_result`] when you need a complete error
71    /// report rather than stopping at the first problem.
72    ///
73    /// # Errors
74    ///
75    /// Returns `Err(errors)` when one or more validation errors were found.
76    pub fn into_errors(self) -> Result<Vec<ValidationWarning>, Vec<ValidationError>> {
77        if self.errors.is_empty() {
78            Ok(self.warnings)
79        } else {
80            Err(self.errors)
81        }
82    }
83}
84
85impl std::fmt::Display for ValidationResult {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        if self.errors.is_empty() && self.warnings.is_empty() {
88            return write!(f, "ok");
89        }
90        for e in &self.errors {
91            writeln!(f, "error: {e}")?;
92        }
93        for w in &self.warnings {
94            writeln!(f, "warning: {}", w.0)?;
95        }
96        Ok(())
97    }
98}
99
100// ── Public API ────────────────────────────────────────────────────────────────
101
102/// Validate a parsed [`Document`], running both structural and semantic checks.
103///
104/// Returns a [`ValidationResult`] that collects all errors and warnings
105/// (rather than stopping at the first problem). Check
106/// [`ValidationResult::is_valid`] to determine whether the document is
107/// conformant.
108///
109/// # Errors
110///
111/// This function does not return `Err`; all findings are collected in the
112/// returned [`ValidationResult`]. Returns `Err` only when a validation
113/// precondition check fails (not currently possible).
114#[allow(unused_variables)]
115pub fn validate(doc: &Document) -> ValidationResult {
116    let mut result = ValidationResult::default();
117    structural::validate(doc, &mut result);
118    semantic::validate(doc, &mut result);
119    result
120}
121
122/// Validate the structural integrity of a specific document without a
123/// [`Document`] enum wrapper.
124///
125/// Validates structural rules (presence of required fields, value ranges).
126pub fn validate_structural<T>(doc: &T) -> Result<(), RedispatchXmlError>
127where
128    T: structural::ValidateStructural,
129{
130    let mut result = ValidationResult::default();
131    doc.validate_structural(&mut result);
132    result
133        .into_result()
134        .map(|_| ())
135        .map_err(|e| RedispatchXmlError::StructuralError(e.to_string()))
136}