Skip to main content

redispatch_xml/
parse.rs

1//! Two-phase XML parsing pipeline for Redispatch 2.0 documents.
2//!
3//! ## Pipeline
4//!
5//! 1. **Detect** — scan the opening bytes of the input to identify the root
6//!    element name and, where present, the `xmlns` namespace.
7//! 2. **Deserialize** — pass the full input to [`quick_xml::de::from_str`].
8//! 3. **Validate namespace** — for document types that carry a `targetNamespace`,
9//!    confirm the detected namespace matches the expected value.
10//!
11//! No libxml2 / XSD validation is performed at parse time. Use the
12//! [`crate::validation`] module for post-parse semantic/structural checks.
13
14use crate::documents::{
15    self, AcknowledgementDocument, ActivationDocument, DocumentType, Kaskade, Kostenblatt,
16    NetworkConstraintDocument, PlannedResourceScheduleDocument, Stammdaten,
17    StatusRequestMarketDocument, UnavailabilityMarketDocument,
18};
19use crate::error::RedispatchXmlError;
20
21// ── Document sum type ─────────────────────────────────────────────────────────
22
23/// A parsed Redispatch 2.0 document (any of the nine supported types).
24#[derive(Debug, Clone, PartialEq)]
25pub enum Document {
26    Activation(Box<ActivationDocument>),
27    PlannedResourceSchedule(Box<PlannedResourceScheduleDocument>),
28    Acknowledgement(Box<AcknowledgementDocument>),
29    Stammdaten(Box<Stammdaten>),
30    StatusRequest(Box<StatusRequestMarketDocument>),
31    Unavailability(Box<UnavailabilityMarketDocument>),
32    Kaskade(Box<Kaskade>),
33    NetworkConstraint(Box<NetworkConstraintDocument>),
34    Kostenblatt(Box<Kostenblatt>),
35}
36
37impl Document {
38    /// Return the [`DocumentType`] variant for this document.
39    pub fn document_type(&self) -> DocumentType {
40        match self {
41            Self::Activation(_) => DocumentType::Activation,
42            Self::PlannedResourceSchedule(_) => DocumentType::PlannedResourceSchedule,
43            Self::Acknowledgement(_) => DocumentType::Acknowledgement,
44            Self::Stammdaten(_) => DocumentType::Stammdaten,
45            Self::StatusRequest(_) => DocumentType::StatusRequest,
46            Self::Unavailability(_) => DocumentType::Unavailability,
47            Self::Kaskade(_) => DocumentType::Kaskade,
48            Self::NetworkConstraint(_) => DocumentType::NetworkConstraint,
49            Self::Kostenblatt(_) => DocumentType::Kostenblatt,
50        }
51    }
52}
53
54// ── From<T> for Document ──────────────────────────────────────────────────────
55
56impl From<ActivationDocument> for Document {
57    fn from(d: ActivationDocument) -> Self {
58        Self::Activation(Box::new(d))
59    }
60}
61impl From<PlannedResourceScheduleDocument> for Document {
62    fn from(d: PlannedResourceScheduleDocument) -> Self {
63        Self::PlannedResourceSchedule(Box::new(d))
64    }
65}
66impl From<AcknowledgementDocument> for Document {
67    fn from(d: AcknowledgementDocument) -> Self {
68        Self::Acknowledgement(Box::new(d))
69    }
70}
71impl From<Stammdaten> for Document {
72    fn from(d: Stammdaten) -> Self {
73        Self::Stammdaten(Box::new(d))
74    }
75}
76impl From<StatusRequestMarketDocument> for Document {
77    fn from(d: StatusRequestMarketDocument) -> Self {
78        Self::StatusRequest(Box::new(d))
79    }
80}
81impl From<UnavailabilityMarketDocument> for Document {
82    fn from(d: UnavailabilityMarketDocument) -> Self {
83        Self::Unavailability(Box::new(d))
84    }
85}
86impl From<Kaskade> for Document {
87    fn from(d: Kaskade) -> Self {
88        Self::Kaskade(Box::new(d))
89    }
90}
91impl From<NetworkConstraintDocument> for Document {
92    fn from(d: NetworkConstraintDocument) -> Self {
93        Self::NetworkConstraint(Box::new(d))
94    }
95}
96impl From<documents::Kostenblatt> for Document {
97    fn from(d: documents::Kostenblatt) -> Self {
98        Self::Kostenblatt(Box::new(d))
99    }
100}
101
102// ── Detection ─────────────────────────────────────────────────────────────────
103
104/// Scan the first 4 KiB of `xml` for the first element start tag and optional
105/// `xmlns` attribute, returning `(root_element_local_name, Option<namespace>)`.
106///
107/// This is intentionally a lightweight byte scan — not a full XML parse — so
108/// that detection is fast even for large documents.
109fn detect_root(xml: &[u8]) -> (String, Option<String>) {
110    // Work with only the first 4096 bytes.
111    let window = &xml[..xml.len().min(4096)];
112    let text = String::from_utf8_lossy(window);
113
114    // Find the first '<' that is not '<?' or '<!'.
115    let mut root_name = String::new();
116    let mut namespace = None;
117
118    for i in 0..text.len() {
119        let ch = text.as_bytes()[i];
120        if ch != b'<' {
121            continue;
122        }
123        let rest = &text[i + 1..];
124        if rest.starts_with('?') || rest.starts_with('!') {
125            continue;
126        }
127        // Extract the local name (up to first space, '>' or '/').
128        let name_end = rest
129            .find(|c: char| c.is_whitespace() || c == '>' || c == '/')
130            .unwrap_or(rest.len());
131        let raw_name = &rest[..name_end];
132        // Strip namespace prefix if present.
133        root_name = if let Some(pos) = raw_name.rfind(':') {
134            raw_name[pos + 1..].to_string()
135        } else {
136            raw_name.to_string()
137        };
138
139        // Scan the opening tag for xmlns="..." or xmlns:xxx="...".
140        let tag_end = rest.find('>').unwrap_or(rest.len());
141        let tag_slice = &rest[..tag_end];
142        namespace = extract_default_namespace(tag_slice);
143        break;
144    }
145
146    (root_name, namespace)
147}
148
149/// Extract the value of the first `xmlns="..."` or `xmlns:xxx="..."` attribute
150/// from a raw tag fragment.
151fn extract_default_namespace(tag: &str) -> Option<String> {
152    // Look for xmlns="..." (default namespace).
153    if let Some(pos) = tag.find("xmlns=\"") {
154        let after = &tag[pos + 7..];
155        if let Some(end) = after.find('"') {
156            return Some(after[..end].to_string());
157        }
158    }
159    // Fall back to xmlns:xxx="..." (prefixed namespace — first occurrence).
160    if let Some(pos) = tag.find("xmlns:") {
161        let after = &tag[pos..];
162        if let Some(eq) = after.find("=\"") {
163            let ns_part = &after[eq + 2..];
164            if let Some(end) = ns_part.find('"') {
165                return Some(ns_part[..end].to_string());
166            }
167        }
168    }
169    None
170}
171
172// ── Public API ────────────────────────────────────────────────────────────────
173
174/// Detect the document type of a Redispatch 2.0 XML message without fully
175/// deserializing it.
176///
177/// # Errors
178///
179/// Returns [`RedispatchXmlError::UnknownDocumentType`] if the root element is
180/// not a recognised Redispatch 2.0 document type.
181pub fn detect(xml: &[u8]) -> Result<DocumentType, RedispatchXmlError> {
182    let (root_name, _) = detect_root(xml);
183    DocumentType::from_root_element(&root_name)
184        .ok_or(RedispatchXmlError::UnknownDocumentType(root_name))
185}
186
187/// Deserialise a Redispatch 2.0 XML document into the appropriate [`Document`]
188/// variant.
189///
190/// The document type is detected automatically from the root element.
191///
192/// # Errors
193///
194/// - [`RedispatchXmlError::UnknownDocumentType`] — unrecognised root element.
195/// - [`RedispatchXmlError::Deserialize`] — XML deserialization failure.
196/// - [`RedispatchXmlError::NamespaceMismatch`] — wrong or missing namespace.
197pub fn parse(xml: &[u8]) -> Result<Document, RedispatchXmlError> {
198    let (root_name, detected_ns) = detect_root(xml);
199    let doc_type = DocumentType::from_root_element(&root_name)
200        .ok_or(RedispatchXmlError::UnknownDocumentType(root_name))?;
201
202    // Validate namespace where required.
203    if let Some(expected_ns) = doc_type.expected_namespace() {
204        match detected_ns.as_deref() {
205            Some(found) if found == expected_ns => {}
206            Some(found) => {
207                return Err(RedispatchXmlError::NamespaceMismatch {
208                    expected: expected_ns,
209                    found: found.to_string(),
210                });
211            }
212            None => {
213                return Err(RedispatchXmlError::NamespaceMismatch {
214                    expected: expected_ns,
215                    found: String::new(),
216                });
217            }
218        }
219    }
220
221    let text =
222        std::str::from_utf8(xml).map_err(|e| RedispatchXmlError::StructuralError(e.to_string()))?;
223
224    match doc_type {
225        DocumentType::Activation => {
226            let doc: ActivationDocument =
227                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
228            Ok(Document::Activation(Box::new(doc)))
229        }
230        DocumentType::PlannedResourceSchedule => {
231            let doc: PlannedResourceScheduleDocument =
232                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
233            Ok(Document::PlannedResourceSchedule(Box::new(doc)))
234        }
235        DocumentType::Acknowledgement => {
236            let doc: AcknowledgementDocument =
237                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
238            Ok(Document::Acknowledgement(Box::new(doc)))
239        }
240        DocumentType::Stammdaten => {
241            let doc: Stammdaten =
242                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
243            Ok(Document::Stammdaten(Box::new(doc)))
244        }
245        DocumentType::StatusRequest => {
246            let doc: StatusRequestMarketDocument =
247                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
248            Ok(Document::StatusRequest(Box::new(doc)))
249        }
250        DocumentType::Unavailability => {
251            let doc: UnavailabilityMarketDocument =
252                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
253            Ok(Document::Unavailability(Box::new(doc)))
254        }
255        DocumentType::Kaskade => {
256            let doc: Kaskade =
257                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
258            Ok(Document::Kaskade(Box::new(doc)))
259        }
260        DocumentType::NetworkConstraint => {
261            let doc: NetworkConstraintDocument =
262                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
263            Ok(Document::NetworkConstraint(Box::new(doc)))
264        }
265        DocumentType::Kostenblatt => {
266            let doc: documents::Kostenblatt =
267                quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)?;
268            Ok(Document::Kostenblatt(Box::new(doc)))
269        }
270    }
271}
272
273/// Deserialise a Redispatch 2.0 XML document into a specific type `T`.
274///
275/// Use this when the document type is known at compile time.
276///
277/// # Errors
278///
279/// Returns [`RedispatchXmlError::Deserialize`] on parse failure.
280pub fn parse_as<T>(xml: &[u8]) -> Result<T, RedispatchXmlError>
281where
282    T: serde::de::DeserializeOwned,
283{
284    let text =
285        std::str::from_utf8(xml).map_err(|e| RedispatchXmlError::StructuralError(e.to_string()))?;
286    quick_xml::de::from_str(text).map_err(RedispatchXmlError::Deserialize)
287}
288
289/// Parse a Redispatch 2.0 XML document **and** run structural + semantic
290/// validation in one step.
291///
292/// Equivalent to calling [`parse`] followed by [`crate::validate`], but more
293/// ergonomic when you always want validation.
294///
295/// # Errors
296///
297/// Returns the first [`RedispatchXmlError`] encountered during parsing.
298/// If parsing succeeds but validation finds errors, returns the first
299/// [`RedispatchXmlError::StructuralError`].
300pub fn parse_and_validate(xml: &[u8]) -> Result<Document, RedispatchXmlError> {
301    let doc = parse(xml)?;
302    let result = crate::validation::validate(&doc);
303    result
304        .into_result()
305        .map(|_| doc)
306        .map_err(|e| RedispatchXmlError::StructuralError(e.to_string()))
307}