Skip to main content

redispatch_xml/validation/
structural.rs

1//! Structural validation — XSD-derivable constraints.
2//!
3//! These rules are pure field-level checks that require no cross-field context:
4//! - `DocumentId` / `Mrid`: 1–35 characters
5//! - `DocumentVersion`: 1–999
6//! - `MarketParticipantId`: exactly 13 decimal digits
7//! - UTC timestamps: offset must be `+00:00`
8//! - `TimeInterval`: end after start
9
10use super::{ValidationError, ValidationResult};
11use crate::parse::Document;
12use crate::types::{DocumentId, DocumentVersion, MarketParticipantId, TimeInterval, UtcDateTime};
13
14// ── Trait for per-type structural validation ──────────────────────────────────
15
16/// Types that can validate their own structural integrity.
17pub trait ValidateStructural {
18    /// Append any structural violations found to `result`.
19    fn validate_structural(&self, result: &mut ValidationResult);
20}
21
22// ── Helper validators ─────────────────────────────────────────────────────────
23
24/// Validate a [`DocumentId`] field.
25pub(crate) fn check_document_id(id: &DocumentId, result: &mut ValidationResult) {
26    let len = id.as_str().len();
27    if len == 0 || len > 35 {
28        result.errors.push(ValidationError::DocumentIdLength(len));
29    }
30}
31
32/// Validate a [`DocumentVersion`] field.
33pub(crate) fn check_document_version(v: &DocumentVersion, result: &mut ValidationResult) {
34    let n = v.get() as u32;
35    if n == 0 || n > 999 {
36        result.errors.push(ValidationError::DocumentVersionRange(n));
37    }
38}
39
40/// Validate a [`MarketParticipantId`] field.
41pub(crate) fn check_participant_id(id: &MarketParticipantId, result: &mut ValidationResult) {
42    let s = id.as_str();
43    if s.len() != 13 || !s.bytes().all(|b| b.is_ascii_digit()) {
44        result
45            .errors
46            .push(ValidationError::MarketParticipantIdFormat(s.to_string()));
47    }
48}
49
50/// Validate that a [`UtcDateTime`] has a UTC offset.
51///
52/// Since [`UtcDateTime::new`] already rejects non-UTC offsets, this is a
53/// belt-and-suspenders check for values that bypassed the constructor.
54pub(crate) fn check_utc_offset(ts: &UtcDateTime, result: &mut ValidationResult) {
55    // UtcDateTime guarantees UTC at construction; no further check needed.
56    let _ = ts;
57    let _ = result;
58}
59
60/// Validate that a [`TimeInterval`] has `end` after `start`.
61pub(crate) fn check_time_interval(interval: &TimeInterval, result: &mut ValidationResult) {
62    if interval.end <= interval.start {
63        result.errors.push(ValidationError::TimeIntervalOrder);
64    }
65}
66
67// ── Top-level dispatcher ──────────────────────────────────────────────────────
68
69/// Run structural checks on any [`Document`] variant.
70pub fn validate(doc: &Document, result: &mut ValidationResult) {
71    match doc {
72        Document::Activation(d) => validate_activation(d, result),
73        Document::PlannedResourceSchedule(d) => validate_prs(d, result),
74        Document::Acknowledgement(d) => validate_ack(d, result),
75        Document::NetworkConstraint(d) => validate_ncd(d, result),
76        Document::Kostenblatt(d) => validate_kostenblatt(d, result),
77        // IEC 62325 documents: validate identifier and revision number.
78        Document::Kaskade(d) => {
79            check_document_id(&d.m_rid, result);
80            check_document_version(&d.revision_number, result);
81        }
82        Document::StatusRequest(d) => {
83            check_document_id(&d.m_rid, result);
84        }
85        Document::Unavailability(d) => {
86            check_document_id(&d.m_rid, result);
87        }
88        Document::Stammdaten(d) => {
89            check_document_id(&d.document_identification, result);
90            check_participant_id(&d.sender.code, result);
91            check_participant_id(&d.empfaenger.code, result);
92        }
93    }
94}
95
96use crate::documents::kostenblatt::Kostenblatt;
97use crate::documents::{
98    AcknowledgementDocument, ActivationDocument, NetworkConstraintDocument,
99    PlannedResourceScheduleDocument,
100};
101
102fn validate_activation(d: &ActivationDocument, result: &mut ValidationResult) {
103    check_document_id(&d.document_identification.v, result);
104    check_document_version(&d.document_version.v, result);
105    check_participant_id(&d.sender_identification.v, result);
106    check_participant_id(&d.receiver_identification.v, result);
107    check_utc_offset(&d.creation_date_time.v, result);
108    check_time_interval(&d.activation_time_interval.v, result);
109}
110
111fn validate_prs(d: &PlannedResourceScheduleDocument, result: &mut ValidationResult) {
112    check_document_id(&d.document_identification.v, result);
113    check_document_version(&d.document_version.v, result);
114    check_participant_id(&d.sender_identification.v, result);
115    check_participant_id(&d.receiver_identification.v, result);
116    check_utc_offset(&d.document_date_time.v, result);
117    check_time_interval(&d.time_period_covered.v, result);
118}
119
120fn validate_ack(d: &AcknowledgementDocument, result: &mut ValidationResult) {
121    check_document_id(&d.document_identification.v, result);
122    check_participant_id(&d.sender_identification.v, result);
123    check_participant_id(&d.receiver_identification.v, result);
124    check_utc_offset(&d.document_date_time.v, result);
125}
126
127fn validate_ncd(d: &NetworkConstraintDocument, result: &mut ValidationResult) {
128    check_document_id(&d.document_identification.v, result);
129    check_document_version(&d.document_version.v, result);
130    check_participant_id(&d.sender_identification.v, result);
131    check_participant_id(&d.receiver_identification.v, result);
132    check_utc_offset(&d.document_date_time.v, result);
133    check_time_interval(&d.time_period_covered.v, result);
134}
135
136fn validate_kostenblatt(d: &Kostenblatt, result: &mut ValidationResult) {
137    check_document_id(&d.document_identification.v, result);
138    check_document_version(&d.document_version.v, result);
139    check_participant_id(&d.sender_identification.v, result);
140    check_participant_id(&d.receiver_identification.v, result);
141    check_utc_offset(&d.document_date_time.v, result);
142    check_time_interval(&d.time_period_covered.v, result);
143}