Skip to main content

zerodds_xml_wire/
validator.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Schema-Validation — DDS-XML 1.0 §6.6.
4//!
5//! Spec §6.6: das XSD muss alle empfangenen Samples validieren. Wir
6//! implementieren minimal-XSD: Element-Namen + Type-Coercion auf
7//! `xs:long`/`xs:double`/`xs:boolean`/`xs:string`/`xs:hexBinary`.
8
9use alloc::collections::BTreeMap;
10use alloc::string::{String, ToString};
11
12use crate::codec::{FieldKind, FieldValue};
13use crate::xsd::XsdGenerator;
14
15/// Validation-Fehler.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ValidationError {
18    /// Required-Field fehlt im Sample.
19    MissingField(String),
20    /// Field hat den falschen Type.
21    TypeMismatch {
22        /// Field-Name.
23        field: String,
24        /// Erwarteter XSD-Type-Name.
25        expected: String,
26        /// Tatsaechlicher FieldKind.
27        actual: FieldKind,
28    },
29    /// Sample enthaelt einen unbekannten Field-Name.
30    UnknownField(String),
31}
32
33impl core::fmt::Display for ValidationError {
34    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35        match self {
36            Self::MissingField(s) => write!(f, "missing required field `{s}`"),
37            Self::TypeMismatch {
38                field,
39                expected,
40                actual,
41            } => write!(f, "field `{field}`: expected {expected}, got {actual:?}"),
42            Self::UnknownField(s) => write!(f, "unknown field `{s}`"),
43        }
44    }
45}
46
47#[cfg(feature = "std")]
48impl std::error::Error for ValidationError {}
49
50/// Validiert ein Sample-Map gegen ein XSD-Generator (= Schema-Builder).
51///
52/// # Errors
53/// Siehe [`ValidationError`].
54pub fn validate(
55    schema: &XsdGenerator,
56    sample: &BTreeMap<String, FieldValue>,
57) -> Result<(), ValidationError> {
58    let xsd = schema.render();
59    // Wir parsen die Felder aus dem XSD-Quelltext direkt; alternativ
60    // koennte XsdGenerator die Felder via Getter exponieren — hier
61    // halten wir die XSD-Quelltext-Sicht als single source.
62    let fields = parse_xsd_fields(&xsd);
63    // 1) UnknownField: jedes sample-Field muss im XSD existieren.
64    for sample_name in sample.keys() {
65        if !fields.iter().any(|(n, _, _)| n == sample_name) {
66            return Err(ValidationError::UnknownField(sample_name.clone()));
67        }
68    }
69    // 2) MissingField + TypeMismatch.
70    for (name, expected_xsd, optional) in &fields {
71        match sample.get(name) {
72            None => {
73                if !optional {
74                    return Err(ValidationError::MissingField(name.clone()));
75                }
76            }
77            Some(v) => {
78                let actual_xsd = field_kind_to_xsd(v.kind());
79                if actual_xsd != expected_xsd.as_str() {
80                    return Err(ValidationError::TypeMismatch {
81                        field: name.clone(),
82                        expected: expected_xsd.clone(),
83                        actual: v.kind(),
84                    });
85                }
86            }
87        }
88    }
89    Ok(())
90}
91
92fn field_kind_to_xsd(k: FieldKind) -> &'static str {
93    match k {
94        FieldKind::Integer => "xs:long",
95        FieldKind::Float => "xs:double",
96        FieldKind::Bool => "xs:boolean",
97        FieldKind::String => "xs:string",
98        FieldKind::Bytes => "xs:hexBinary",
99    }
100}
101
102fn parse_xsd_fields(xsd: &str) -> alloc::vec::Vec<(String, String, bool)> {
103    let mut out = alloc::vec::Vec::new();
104    let mut cursor = 0;
105    while let Some(s) = xsd[cursor..].find("<xs:element name=\"") {
106        let start = cursor + s + "<xs:element name=\"".len();
107        let name_end = match xsd[start..].find('"') {
108            Some(e) => e,
109            None => break,
110        };
111        let name = xsd[start..start + name_end].to_string();
112        let after_name = start + name_end + 1;
113        // Skip the schema's root element (it has type only via complexType).
114        if !xsd[after_name..]
115            .lines()
116            .next()
117            .unwrap_or("")
118            .contains("type=\"")
119        {
120            cursor = after_name;
121            continue;
122        }
123        let type_marker = "type=\"";
124        let type_start = match xsd[after_name..].find(type_marker) {
125            Some(t) => after_name + t + type_marker.len(),
126            None => break,
127        };
128        let type_end = match xsd[type_start..].find('"') {
129            Some(e) => e,
130            None => break,
131        };
132        let xsd_type = xsd[type_start..type_start + type_end].to_string();
133        // Look for minOccurs="0" on same line up to "/>".
134        let line_end = xsd[after_name..]
135            .find("/>")
136            .map(|p| after_name + p)
137            .unwrap_or(xsd.len());
138        let optional = xsd[after_name..line_end].contains(r#"minOccurs="0""#);
139        out.push((name, xsd_type, optional));
140        cursor = line_end;
141    }
142    out
143}
144
145#[cfg(test)]
146#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
147mod tests {
148    use super::*;
149    use crate::xsd::XsdType;
150
151    fn schema() -> XsdGenerator {
152        XsdGenerator::new("Trade")
153            .field("id", XsdType::Long, false)
154            .field("price", XsdType::Double, false)
155            .field("symbol", XsdType::String, true)
156    }
157
158    #[test]
159    fn valid_sample_passes() {
160        let mut s = BTreeMap::new();
161        s.insert("id".into(), FieldValue::Integer(1));
162        s.insert("price".into(), FieldValue::Float(2.0));
163        s.insert("symbol".into(), FieldValue::String("X".into()));
164        validate(&schema(), &s).unwrap();
165    }
166
167    #[test]
168    fn optional_field_can_be_absent() {
169        let mut s = BTreeMap::new();
170        s.insert("id".into(), FieldValue::Integer(1));
171        s.insert("price".into(), FieldValue::Float(2.0));
172        validate(&schema(), &s).unwrap();
173    }
174
175    #[test]
176    fn missing_required_field_rejected() {
177        let mut s = BTreeMap::new();
178        s.insert("id".into(), FieldValue::Integer(1));
179        let err = validate(&schema(), &s).unwrap_err();
180        assert!(matches!(err, ValidationError::MissingField(ref f) if f == "price"));
181    }
182
183    #[test]
184    fn unknown_field_rejected() {
185        let mut s = BTreeMap::new();
186        s.insert("id".into(), FieldValue::Integer(1));
187        s.insert("price".into(), FieldValue::Float(2.0));
188        s.insert("ghost".into(), FieldValue::Integer(0));
189        let err = validate(&schema(), &s).unwrap_err();
190        assert!(matches!(err, ValidationError::UnknownField(_)));
191    }
192
193    #[test]
194    fn type_mismatch_rejected() {
195        let mut s = BTreeMap::new();
196        s.insert("id".into(), FieldValue::String("not-a-number".into()));
197        s.insert("price".into(), FieldValue::Float(2.0));
198        let err = validate(&schema(), &s).unwrap_err();
199        assert!(matches!(err, ValidationError::TypeMismatch { .. }));
200    }
201}