1use miette::Diagnostic;
34use thiserror::Error;
35
36pub const SBE_XSD: &str = include_str!("xsd/sbe.xsd");
40
41#[derive(Debug, Error, Diagnostic)]
43pub enum XsdValidationError {
44 #[error("XML parse error: {0}")]
46 #[diagnostic(code(ergo_sbe::xsd::malformed))]
47 MalformedXml(String),
48
49 #[error("root element must be messageSchema, found `{found}`")]
51 #[diagnostic(code(ergo_sbe::xsd::bad_root))]
52 BadRoot {
53 found: String,
55 },
56
57 #[error("messageSchema is missing required attribute `{attr}`")]
59 #[diagnostic(code(ergo_sbe::xsd::missing_attr))]
60 MissingAttribute {
61 attr: &'static str,
63 },
64
65 #[error("element `{element}` is not allowed under `{parent}` by sbe.xsd")]
67 #[diagnostic(code(ergo_sbe::xsd::unexpected_element))]
68 UnexpectedElement {
69 parent: String,
71 element: String,
73 },
74
75 #[error("attribute `{attr}` is not allowed on `{element}` by sbe.xsd")]
77 #[diagnostic(code(ergo_sbe::xsd::unexpected_attr))]
78 UnexpectedAttribute {
79 element: String,
81 attr: String,
83 },
84}
85
86fn local_name(tag: &str) -> &str {
87 tag.rsplit(':').next().unwrap_or(tag)
88}
89
90pub fn validate_against_sbe_xsd(xml: &str) -> Result<(), XsdValidationError> {
113 let doc = roxmltree::Document::parse(xml)
114 .map_err(|e| XsdValidationError::MalformedXml(e.to_string()))?;
115 let root = doc.root_element();
116 let root_name = local_name(root.tag_name().name());
117 if root_name != "messageSchema" {
118 return Err(XsdValidationError::BadRoot {
119 found: root_name.to_string(),
120 });
121 }
122
123 if root.attribute("id").is_none() {
133 return Err(XsdValidationError::MissingAttribute { attr: "id" });
134 }
135 check_attrs("messageSchema", root, crate::schema_attrs::MESSAGE_SCHEMA)?;
136
137 for child in root.children().filter(|n| n.is_element()) {
138 let name = local_name(child.tag_name().name());
139 match name {
140 "types" => validate_types(child)?,
141 "message" => validate_message(child)?,
142 "include" => {}
144 other => {
145 return Err(XsdValidationError::UnexpectedElement {
146 parent: "messageSchema".into(),
147 element: other.into(),
148 });
149 }
150 }
151 }
152 Ok(())
153}
154
155fn check_attrs(
156 element: &str,
157 node: roxmltree::Node<'_, '_>,
158 allowed: &[&str],
159) -> Result<(), XsdValidationError> {
160 for attr in node.attributes() {
161 if attr.namespace().is_some() {
166 continue;
167 }
168 let name = local_name(attr.name());
169 if name.starts_with("xmlns") {
170 continue;
171 }
172 if !allowed.contains(&name) {
173 return Err(XsdValidationError::UnexpectedAttribute {
174 element: element.into(),
175 attr: name.into(),
176 });
177 }
178 }
179 Ok(())
180}
181
182fn validate_types(node: roxmltree::Node<'_, '_>) -> Result<(), XsdValidationError> {
183 check_attrs("types", node, &["package"])?;
186 for child in node.children().filter(|n| n.is_element()) {
187 let name = local_name(child.tag_name().name());
188 match name {
189 "type" => check_attrs(
190 "type",
191 child,
192 &[
193 "name",
194 "primitiveType",
195 "length",
196 "presence",
197 "nullValue",
198 "minValue",
199 "maxValue",
200 "characterEncoding",
201 "epoch",
202 "timeUnit",
203 "unit",
205 "semanticType",
206 "description",
207 "sinceVersion",
208 "deprecated",
209 "offset",
210 "valueRef",
211 ],
212 )?,
213 "composite" => validate_composite(child)?,
214 "enum" => validate_enum(child)?,
215 "set" => validate_set(child)?,
216 other => {
217 return Err(XsdValidationError::UnexpectedElement {
218 parent: "types".into(),
219 element: other.into(),
220 });
221 }
222 }
223 }
224 Ok(())
225}
226
227fn validate_composite(node: roxmltree::Node<'_, '_>) -> Result<(), XsdValidationError> {
228 check_attrs(
229 "composite",
230 node,
231 &[
232 "name",
233 "description",
234 "semanticType",
235 "sinceVersion",
236 "deprecated",
237 "offset",
238 ],
239 )?;
240 for child in node.children().filter(|n| n.is_element()) {
241 let name = local_name(child.tag_name().name());
242 match name {
243 "type" | "enum" | "set" | "ref" | "composite" => {}
244 "description" | "comment" => {}
245 other => {
246 return Err(XsdValidationError::UnexpectedElement {
247 parent: "composite".into(),
248 element: other.into(),
249 });
250 }
251 }
252 }
253 Ok(())
254}
255
256fn validate_enum(node: roxmltree::Node<'_, '_>) -> Result<(), XsdValidationError> {
257 check_attrs("enum", node, crate::schema_attrs::ENUM)?;
258 for child in node.children().filter(|n| n.is_element()) {
259 let name = local_name(child.tag_name().name());
260 match name {
261 "validValue" => check_attrs(
262 "validValue",
263 child,
264 &[
266 "name",
267 "description",
268 "sinceVersion",
269 "deprecated",
270 "jsonValue",
271 ],
272 )?,
273 "description" | "comment" => {}
274 other => {
275 return Err(XsdValidationError::UnexpectedElement {
276 parent: "enum".into(),
277 element: other.into(),
278 });
279 }
280 }
281 }
282 Ok(())
283}
284
285fn validate_set(node: roxmltree::Node<'_, '_>) -> Result<(), XsdValidationError> {
286 check_attrs(
287 "set",
288 node,
289 &[
290 "name",
291 "encodingType",
292 "description",
293 "sinceVersion",
294 "deprecated",
295 "semanticType",
296 ],
297 )?;
298 for child in node.children().filter(|n| n.is_element()) {
299 let name = local_name(child.tag_name().name());
300 match name {
301 "choice" => check_attrs(
302 "choice",
303 child,
304 &[
306 "name",
307 "description",
308 "sinceVersion",
309 "deprecated",
310 "jsonValue",
311 ],
312 )?,
313 "description" | "comment" => {}
314 other => {
315 return Err(XsdValidationError::UnexpectedElement {
316 parent: "set".into(),
317 element: other.into(),
318 });
319 }
320 }
321 }
322 Ok(())
323}
324
325fn validate_message(node: roxmltree::Node<'_, '_>) -> Result<(), XsdValidationError> {
326 check_attrs("message", node, crate::schema_attrs::MESSAGE)?;
327 for child in node.children().filter(|n| n.is_element()) {
328 let name = local_name(child.tag_name().name());
329 match name {
330 "field" => check_attrs("field", child, crate::schema_attrs::FIELD_LIKE)?,
331 "group" => validate_group(child)?,
332 "data" => check_attrs("data", child, crate::schema_attrs::FIELD_LIKE)?,
333 "description" | "comment" => {}
334 other => {
335 return Err(XsdValidationError::UnexpectedElement {
336 parent: "message".into(),
337 element: other.into(),
338 });
339 }
340 }
341 }
342 Ok(())
343}
344
345fn validate_group(node: roxmltree::Node<'_, '_>) -> Result<(), XsdValidationError> {
346 check_attrs("group", node, crate::schema_attrs::GROUP)?;
347 for child in node.children().filter(|n| n.is_element()) {
348 let name = local_name(child.tag_name().name());
349 match name {
350 "field" | "group" | "data" | "description" | "comment" => {}
351 other => {
352 return Err(XsdValidationError::UnexpectedElement {
353 parent: "group".into(),
354 element: other.into(),
355 });
356 }
357 }
358 }
359 Ok(())
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn accepts_minimal_valid_schema() -> Result<(), Box<dyn std::error::Error>> {
368 let xml = r#"<?xml version="1.0"?>
369 <messageSchema package="t" id="1" version="0" byteOrder="littleEndian">
370 <types>
371 <composite name="messageHeader">
372 <type name="blockLength" primitiveType="uint16"/>
373 <type name="templateId" primitiveType="uint16"/>
374 <type name="schemaId" primitiveType="uint16"/>
375 <type name="version" primitiveType="uint16"/>
376 </composite>
377 <type name="u32" primitiveType="uint32"/>
378 </types>
379 <message name="M" id="1">
380 <field name="x" id="1" type="u32"/>
381 </message>
382 </messageSchema>"#;
383 validate_against_sbe_xsd(xml)?;
384 Ok(())
385 }
386
387 #[test]
388 fn rejects_bad_root() {
389 let xml = r#"<?xml version="1.0"?><notSchema id="1" version="0"/>"#;
390 assert!(matches!(
391 validate_against_sbe_xsd(xml),
392 Err(XsdValidationError::BadRoot { .. })
393 ));
394 }
395
396 #[test]
397 fn rejects_unknown_message_child() {
398 let xml = r#"<?xml version="1.0"?>
399 <messageSchema id="1" version="0">
400 <types/>
401 <message name="M" id="1">
402 <notAField name="x"/>
403 </message>
404 </messageSchema>"#;
405 assert!(matches!(
406 validate_against_sbe_xsd(xml),
407 Err(XsdValidationError::UnexpectedElement { .. })
408 ));
409 }
410
411 #[test]
412 fn accepts_enum_set_group_and_var_data_shapes() -> Result<(), Box<dyn std::error::Error>> {
413 let xml = r#"<?xml version="1.0"?>
414 <messageSchema package="t" id="1" version="0">
415 <types>
416 <composite name="messageHeader">
417 <type name="blockLength" primitiveType="uint16"/>
418 <type name="templateId" primitiveType="uint16"/>
419 <type name="schemaId" primitiveType="uint16"/>
420 <type name="version" primitiveType="uint16"/>
421 </composite>
422 <composite name="groupSizeEncoding">
423 <type name="blockLength" primitiveType="uint16"/>
424 <type name="numInGroup" primitiveType="uint16"/>
425 </composite>
426 <composite name="varStringEncoding">
427 <type name="length" primitiveType="uint32"/>
428 <type name="varData" primitiveType="uint8" length="0"/>
429 </composite>
430 <enum name="Side" encodingType="uint8">
431 <validValue name="Buy">1</validValue>
432 <validValue name="Sell">2</validValue>
433 </enum>
434 <set name="Flags" encodingType="uint8">
435 <choice name="Firm">0</choice>
436 </set>
437 </types>
438 <message name="Order" id="1">
439 <field name="side" id="1" type="Side"/>
440 <group name="fills" id="2" dimensionType="groupSizeEncoding">
441 <field name="quantity" id="3" type="uint32"/>
442 <data name="venue" id="4" type="varStringEncoding"/>
443 </group>
444 <data name="account" id="5" type="varStringEncoding"/>
445 </message>
446 </messageSchema>"#;
447
448 validate_against_sbe_xsd(xml)?;
449 Ok(())
450 }
451
452 #[test]
453 fn rejects_malformed_missing_attributes_and_unknown_type_shapes() {
454 type ValidationCase<'a> = (&'a str, &'a str, fn(&XsdValidationError) -> bool);
455 let cases: [ValidationCase<'_>; 4] = [
456 (
457 "<messageSchema",
458 "malformed XML",
459 |error: &XsdValidationError| matches!(error, XsdValidationError::MalformedXml(_)),
460 ),
461 (
462 r#"<messageSchema package="t" version="0"/>"#,
463 "missing schema id",
464 |error: &XsdValidationError| {
465 matches!(error, XsdValidationError::MissingAttribute { attr: "id" })
466 },
467 ),
468 (
469 r#"<messageSchema package="t" id="1" version="0" surprise="yes"/>"#,
470 "unknown root attribute",
471 |error: &XsdValidationError| {
472 matches!(error, XsdValidationError::UnexpectedAttribute { .. })
473 },
474 ),
475 (
476 r#"<messageSchema package="t" id="1" version="0"><types><unknown/></types></messageSchema>"#,
477 "unknown type element",
478 |error: &XsdValidationError| {
479 matches!(error, XsdValidationError::UnexpectedElement { .. })
480 },
481 ),
482 ];
483
484 for (xml, context, predicate) in cases {
485 let result = validate_against_sbe_xsd(xml);
486 assert!(
487 result.as_ref().is_err_and(predicate),
488 "{context}: {result:?}"
489 );
490 }
491 }
492
493 #[test]
494 fn embedded_xsd_is_present() {
495 assert!(SBE_XSD.contains("messageSchema"));
496 assert!(SBE_XSD.contains("xs:schema"));
497 }
498
499 fn enum_null_schema(encoding: &str, null_value: &str) -> String {
500 format!(
501 r#"<?xml version="1.0"?>
502 <messageSchema package="t" id="1" version="0" byteOrder="littleEndian">
503 <types>
504 <composite name="messageHeader">
505 <type name="blockLength" primitiveType="uint16"/>
506 <type name="templateId" primitiveType="uint16"/>
507 <type name="schemaId" primitiveType="uint16"/>
508 <type name="version" primitiveType="uint16"/>
509 </composite>
510 <enum name="Code" encodingType="{encoding}" nullValue="{null_value}">
511 <validValue name="Ok">0</validValue>
512 </enum>
513 </types>
514 <message name="M" id="1">
515 <field name="code" id="1" type="Code"/>
516 </message>
517 </messageSchema>"#
518 )
519 }
520
521 #[test]
522 fn accepts_unsigned_enum_null_value() -> Result<(), Box<dyn std::error::Error>> {
523 let xml = enum_null_schema("uint8", "99");
524 validate_against_sbe_xsd(&xml)?;
525 crate::parse_with_xsd_validation(&xml)?;
526 Ok(())
527 }
528
529 #[test]
530 fn accepts_signed_enum_null_value() -> Result<(), Box<dyn std::error::Error>> {
531 let xml = enum_null_schema("int8", "-1");
532 validate_against_sbe_xsd(&xml)?;
533 crate::parse_with_xsd_validation(&xml)?;
534 Ok(())
535 }
536
537 #[test]
538 fn rejects_unknown_non_namespaced_enum_attribute() {
539 let xml = r#"<?xml version="1.0"?>
540 <messageSchema package="t" id="1" version="0">
541 <types>
542 <enum name="Code" encodingType="uint8" surprise="yes">
543 <validValue name="Ok">0</validValue>
544 </enum>
545 </types>
546 </messageSchema>"#;
547 assert!(matches!(
548 validate_against_sbe_xsd(xml),
549 Err(XsdValidationError::UnexpectedAttribute {
550 element,
551 attr
552 }) if element == "enum" && attr == "surprise"
553 ));
554 }
555}