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}