Skip to main content

ironsbe_schema/
validation.rs

1//! Schema validation utilities.
2//!
3//! This module provides validation functions for SBE schemas to ensure
4//! correctness and consistency.
5
6use crate::error::SchemaError;
7use crate::types::Schema;
8
9/// Validates a parsed schema for correctness.
10///
11/// # Arguments
12/// * `schema` - The schema to validate
13///
14/// # Returns
15/// Ok(()) if valid, or SchemaError describing the issue.
16///
17/// # Errors
18/// Returns `SchemaError` if validation fails.
19pub fn validate_schema(schema: &Schema) -> Result<(), SchemaError> {
20    validate_types(schema)?;
21    validate_messages(schema)?;
22    Ok(())
23}
24
25/// Validates all type definitions in the schema.
26fn validate_types(schema: &Schema) -> Result<(), SchemaError> {
27    for type_def in &schema.types {
28        match type_def {
29            crate::types::TypeDef::Composite(composite) => {
30                validate_composite(schema, composite)?;
31            }
32            crate::types::TypeDef::Enum(enum_def) => {
33                validate_enum(enum_def)?;
34            }
35            crate::types::TypeDef::Set(set_def) => {
36                validate_set(set_def)?;
37            }
38            _ => {}
39        }
40    }
41    Ok(())
42}
43
44/// Validates a composite type definition.
45fn validate_composite(
46    _schema: &Schema,
47    composite: &crate::types::CompositeDef,
48) -> Result<(), SchemaError> {
49    let mut expected_offset = 0;
50
51    for field in &composite.fields {
52        if let Some(offset) = field.offset {
53            if offset < expected_offset {
54                return Err(SchemaError::InvalidOffset {
55                    field: field.name.clone(),
56                    offset,
57                });
58            }
59            expected_offset = offset + field.encoded_length;
60        } else {
61            expected_offset += field.encoded_length;
62        }
63    }
64
65    Ok(())
66}
67
68/// Validates an enum type definition.
69fn validate_enum(enum_def: &crate::types::EnumDef) -> Result<(), SchemaError> {
70    use std::collections::HashSet;
71
72    let mut seen_names = HashSet::new();
73    let mut seen_values = HashSet::new();
74
75    for value in &enum_def.valid_values {
76        if !seen_names.insert(&value.name) {
77            return Err(SchemaError::Validation {
78                message: format!(
79                    "Duplicate enum value name '{}' in enum '{}'",
80                    value.name, enum_def.name
81                ),
82            });
83        }
84
85        if !seen_values.insert(&value.value) {
86            return Err(SchemaError::Validation {
87                message: format!(
88                    "Duplicate enum value '{}' in enum '{}'",
89                    value.value, enum_def.name
90                ),
91            });
92        }
93    }
94
95    Ok(())
96}
97
98/// Validates a set type definition.
99fn validate_set(set_def: &crate::types::SetDef) -> Result<(), SchemaError> {
100    use std::collections::HashSet;
101
102    let max_bits = set_def.encoding_type.size() * 8;
103    let mut seen_positions = HashSet::new();
104
105    for choice in &set_def.choices {
106        if choice.bit_position as usize >= max_bits {
107            return Err(SchemaError::Validation {
108                message: format!(
109                    "Bit position {} exceeds maximum {} for set '{}'",
110                    choice.bit_position,
111                    max_bits - 1,
112                    set_def.name
113                ),
114            });
115        }
116
117        if !seen_positions.insert(choice.bit_position) {
118            return Err(SchemaError::Validation {
119                message: format!(
120                    "Duplicate bit position {} in set '{}'",
121                    choice.bit_position, set_def.name
122                ),
123            });
124        }
125    }
126
127    Ok(())
128}
129
130/// Validates all message definitions in the schema.
131fn validate_messages(schema: &Schema) -> Result<(), SchemaError> {
132    use std::collections::HashSet;
133
134    let mut seen_ids = HashSet::new();
135    let mut seen_names = HashSet::new();
136
137    for msg in &schema.messages {
138        if !seen_ids.insert(msg.id) {
139            return Err(SchemaError::Validation {
140                message: format!("Duplicate message ID {} for message '{}'", msg.id, msg.name),
141            });
142        }
143
144        if !seen_names.insert(&msg.name) {
145            return Err(SchemaError::Validation {
146                message: format!("Duplicate message name '{}'", msg.name),
147            });
148        }
149
150        validate_message_fields(schema, msg)?;
151    }
152
153    Ok(())
154}
155
156/// Validates fields within a message.
157fn validate_message_fields(
158    schema: &Schema,
159    msg: &crate::messages::MessageDef,
160) -> Result<(), SchemaError> {
161    let mut max_offset = 0;
162
163    for field in &msg.fields {
164        // Check type exists
165        if !schema.has_type(&field.type_name) {
166            // Check if it's a built-in primitive
167            if crate::types::PrimitiveType::from_sbe_name(&field.type_name).is_none() {
168                return Err(SchemaError::TypeNotFound {
169                    name: field.type_name.clone(),
170                });
171            }
172        }
173
174        // Check offset ordering
175        if field.offset < max_offset && field.encoded_length > 0 {
176            return Err(SchemaError::InvalidOffset {
177                field: field.name.clone(),
178                offset: field.offset,
179            });
180        }
181
182        max_offset = field.offset + field.encoded_length;
183    }
184
185    // Check block length
186    if max_offset > msg.block_length as usize {
187        return Err(SchemaError::BlockLengthMismatch {
188            message: msg.name.clone(),
189            declared: msg.block_length,
190            calculated: max_offset as u16,
191        });
192    }
193
194    // Validate groups
195    for group in &msg.groups {
196        validate_group_fields(schema, group)?;
197    }
198
199    Ok(())
200}
201
202/// Validates fields within a group.
203fn validate_group_fields(
204    schema: &Schema,
205    group: &crate::messages::GroupDef,
206) -> Result<(), SchemaError> {
207    for field in &group.fields {
208        if !schema.has_type(&field.type_name)
209            && crate::types::PrimitiveType::from_sbe_name(&field.type_name).is_none()
210        {
211            return Err(SchemaError::TypeNotFound {
212                name: field.type_name.clone(),
213            });
214        }
215    }
216
217    // Validate nested groups
218    for nested in &group.nested_groups {
219        validate_group_fields(schema, nested)?;
220    }
221
222    Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::parser::parse_schema;
229
230    #[test]
231    fn test_validate_valid_schema() {
232        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
233<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
234                   package="test" id="1" version="1" byteOrder="littleEndian">
235    <types>
236        <type name="uint64" primitiveType="uint64"/>
237    </types>
238    <sbe:message name="Test" id="1" blockLength="8">
239        <field name="value" id="1" type="uint64" offset="0"/>
240    </sbe:message>
241</sbe:messageSchema>"#;
242
243        let schema = parse_schema(xml).expect("Failed to parse");
244        assert!(validate_schema(&schema).is_ok());
245    }
246
247    #[test]
248    fn test_validate_duplicate_message_id() {
249        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
250<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
251                   package="test" id="1" version="1" byteOrder="littleEndian">
252    <types>
253        <type name="uint64" primitiveType="uint64"/>
254    </types>
255    <sbe:message name="Test1" id="1" blockLength="8">
256        <field name="value" id="1" type="uint64" offset="0"/>
257    </sbe:message>
258    <sbe:message name="Test2" id="1" blockLength="8">
259        <field name="value" id="1" type="uint64" offset="0"/>
260    </sbe:message>
261</sbe:messageSchema>"#;
262
263        let schema = parse_schema(xml).expect("Failed to parse");
264        let result = validate_schema(&schema);
265        assert!(result.is_err());
266    }
267
268    #[test]
269    fn test_validate_duplicate_message_name() {
270        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
271<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
272                   package="test" id="1" version="1" byteOrder="littleEndian">
273    <types>
274        <type name="uint64" primitiveType="uint64"/>
275    </types>
276    <sbe:message name="Test" id="1" blockLength="8">
277        <field name="value" id="1" type="uint64" offset="0"/>
278    </sbe:message>
279    <sbe:message name="Test" id="2" blockLength="8">
280        <field name="value" id="1" type="uint64" offset="0"/>
281    </sbe:message>
282</sbe:messageSchema>"#;
283
284        let schema = parse_schema(xml).expect("Failed to parse");
285        let result = validate_schema(&schema);
286        assert!(result.is_err());
287    }
288
289    #[test]
290    fn test_validate_unknown_field_type() {
291        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
292<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
293                   package="test" id="1" version="1" byteOrder="littleEndian">
294    <types>
295    </types>
296    <sbe:message name="Test" id="1" blockLength="8">
297        <field name="value" id="1" type="UnknownType" offset="0"/>
298    </sbe:message>
299</sbe:messageSchema>"#;
300
301        let schema = parse_schema(xml).expect("Failed to parse");
302        let result = validate_schema(&schema);
303        assert!(result.is_err());
304    }
305
306    #[test]
307    fn test_validate_primitive_type_field() {
308        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
309<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
310                   package="test" id="1" version="1" byteOrder="littleEndian">
311    <types>
312    </types>
313    <sbe:message name="Test" id="1" blockLength="8">
314        <field name="value" id="1" type="uint64" offset="0"/>
315    </sbe:message>
316</sbe:messageSchema>"#;
317
318        let schema = parse_schema(xml).expect("Failed to parse");
319        let result = validate_schema(&schema);
320        assert!(result.is_ok());
321    }
322
323    #[test]
324    fn test_validate_enum_with_valid_values() {
325        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
326<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
327                   package="test" id="1" version="1" byteOrder="littleEndian">
328    <types>
329        <enum name="Side" encodingType="uint8">
330            <validValue name="Buy">1</validValue>
331            <validValue name="Sell">2</validValue>
332        </enum>
333    </types>
334    <sbe:message name="Test" id="1" blockLength="1">
335        <field name="side" id="1" type="Side" offset="0"/>
336    </sbe:message>
337</sbe:messageSchema>"#;
338
339        let schema = parse_schema(xml).expect("Failed to parse");
340        let result = validate_schema(&schema);
341        assert!(result.is_ok());
342    }
343
344    #[test]
345    fn test_validate_set_type() {
346        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
347<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
348                   package="test" id="1" version="1" byteOrder="littleEndian">
349    <types>
350        <set name="Flags" encodingType="uint8">
351            <choice name="Active">0</choice>
352            <choice name="Visible">1</choice>
353        </set>
354    </types>
355    <sbe:message name="Test" id="1" blockLength="1">
356        <field name="flags" id="1" type="Flags" offset="0"/>
357    </sbe:message>
358</sbe:messageSchema>"#;
359
360        let schema = parse_schema(xml).expect("Failed to parse");
361        let result = validate_schema(&schema);
362        assert!(result.is_ok());
363    }
364
365    #[test]
366    fn test_validate_composite_type() {
367        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
368<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
369                   package="test" id="1" version="1" byteOrder="littleEndian">
370    <types>
371        <composite name="Decimal">
372            <type name="mantissa" primitiveType="int64"/>
373            <type name="exponent" primitiveType="int8"/>
374        </composite>
375    </types>
376    <sbe:message name="Test" id="1" blockLength="9">
377        <field name="price" id="1" type="Decimal" offset="0"/>
378    </sbe:message>
379</sbe:messageSchema>"#;
380
381        let schema = parse_schema(xml).expect("Failed to parse");
382        let result = validate_schema(&schema);
383        assert!(result.is_ok());
384    }
385}