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