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    pub fn into_result(mut self) -> Result<Vec<ValidationWarning>, ValidationError> {
59        if self.errors.is_empty() {
60            Ok(self.warnings)
61        } else {
62            Err(self.errors.remove(0))
63        }
64    }
65}
66
67impl std::fmt::Display for ValidationResult {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        if self.errors.is_empty() && self.warnings.is_empty() {
70            return write!(f, "ok");
71        }
72        for e in &self.errors {
73            writeln!(f, "error: {e}")?;
74        }
75        for w in &self.warnings {
76            writeln!(f, "warning: {}", w.0)?;
77        }
78        Ok(())
79    }
80}
81
82// ── Public API ────────────────────────────────────────────────────────────────
83
84/// Validate a parsed [`Document`], running both structural and semantic checks.
85///
86/// Returns a [`ValidationResult`] that collects all errors and warnings
87/// (rather than stopping at the first problem). Check
88/// [`ValidationResult::is_valid`] to determine whether the document is
89/// conformant.
90///
91/// # Errors
92///
93/// This function does not return `Err`; all findings are collected in the
94/// returned [`ValidationResult`]. Returns `Err` only when a validation
95/// precondition check fails (not currently possible).
96#[allow(unused_variables)]
97pub fn validate(doc: &Document) -> ValidationResult {
98    let mut result = ValidationResult::default();
99    structural::validate(doc, &mut result);
100    semantic::validate(doc, &mut result);
101    result
102}
103
104/// Validate the structural integrity of a specific document without a
105/// [`Document`] enum wrapper.
106///
107/// Validates structural rules (presence of required fields, value ranges).
108pub fn validate_structural<T>(doc: &T) -> Result<(), RedispatchXmlError>
109where
110    T: structural::ValidateStructural,
111{
112    let mut result = ValidationResult::default();
113    doc.validate_structural(&mut result);
114    result
115        .into_result()
116        .map(|_| ())
117        .map_err(|e| RedispatchXmlError::StructuralError(e.to_string()))
118}