zerodds_xml_wire/
validator.rs1use alloc::collections::BTreeMap;
10use alloc::string::{String, ToString};
11
12use crate::codec::{FieldKind, FieldValue};
13use crate::xsd::XsdGenerator;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ValidationError {
18 MissingField(String),
20 TypeMismatch {
22 field: String,
24 expected: String,
26 actual: FieldKind,
28 },
29 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
50pub fn validate(
55 schema: &XsdGenerator,
56 sample: &BTreeMap<String, FieldValue>,
57) -> Result<(), ValidationError> {
58 let xsd = schema.render();
59 let fields = parse_xsd_fields(&xsd);
63 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 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 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 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}