Skip to main content

quickfix_tokio/
datadictionary.rs

1//! FIX data dictionary: loads the standard QuickFIX XML specs
2//! (`spec/FIX44.xml` etc.) and validates messages against them.
3
4use std::collections::{HashMap, HashSet};
5
6use quick_xml::events::Event;
7
8use crate::error::{Error, RejectError, Result, SessionRejectReason};
9use crate::field_map::GroupTemplate;
10use crate::message::{Message, Tag};
11use crate::tags;
12
13/// Tags >= this are user-defined per the FIX spec.
14pub const USER_DEFINED_TAG_MIN: Tag = 5000;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FieldType {
18    Int,
19    Length,
20    SeqNum,
21    NumInGroup,
22    DayOfMonth,
23    Float,
24    Qty,
25    Price,
26    PriceOffset,
27    Amt,
28    Percentage,
29    Char,
30    Boolean,
31    String,
32    Data,
33    UtcTimestamp,
34    UtcDateOnly,
35    UtcTimeOnly,
36    LocalMktDate,
37    MonthYear,
38    Other,
39}
40
41impl FieldType {
42    fn from_name(name: &str) -> Self {
43        match name {
44            "INT" => Self::Int,
45            "LENGTH" => Self::Length,
46            "SEQNUM" => Self::SeqNum,
47            "NUMINGROUP" => Self::NumInGroup,
48            "DAYOFMONTH" => Self::DayOfMonth,
49            "FLOAT" => Self::Float,
50            "QTY" | "QUANTITY" => Self::Qty,
51            "PRICE" => Self::Price,
52            "PRICEOFFSET" => Self::PriceOffset,
53            "AMT" => Self::Amt,
54            "PERCENTAGE" => Self::Percentage,
55            "CHAR" => Self::Char,
56            "BOOLEAN" => Self::Boolean,
57            "STRING" | "MULTIPLEVALUESTRING" | "MULTIPLESTRINGVALUE" | "MULTIPLECHARVALUE"
58            | "COUNTRY" | "CURRENCY" | "EXCHANGE" | "LANGUAGE" => Self::String,
59            "DATA" | "XMLDATA" => Self::Data,
60            "UTCTIMESTAMP" | "TIME" => Self::UtcTimestamp,
61            "UTCDATEONLY" | "UTCDATE" | "DATE" => Self::UtcDateOnly,
62            "UTCTIMEONLY" => Self::UtcTimeOnly,
63            "LOCALMKTDATE" => Self::LocalMktDate,
64            "MONTHYEAR" => Self::MonthYear,
65            _ => Self::Other,
66        }
67    }
68}
69
70#[derive(Debug, Clone)]
71pub struct FieldDef {
72    pub tag: Tag,
73    pub name: String,
74    pub field_type: FieldType,
75    /// Allowed values (enum fields); empty = unrestricted.
76    pub values: HashSet<String>,
77    /// Enum values with their descriptions, in spec order (for codegen).
78    pub enum_values: Vec<(String, String)>,
79}
80
81#[derive(Debug, Clone, Default)]
82pub struct GroupDef {
83    pub counter: Tag,
84    pub delimiter: Tag,
85    /// All member tags, including nested groups' tags.
86    pub tags: HashSet<Tag>,
87    pub required: Vec<Tag>,
88    pub groups: HashMap<Tag, GroupDef>,
89    /// Direct members in spec order (nested groups appear as their counter).
90    pub member_order: Vec<Tag>,
91}
92
93#[derive(Debug, Clone, Default)]
94pub struct MessageDef {
95    pub name: String,
96    pub msg_type: String,
97    pub tags: HashSet<Tag>,
98    pub required: Vec<Tag>,
99    pub groups: HashMap<Tag, GroupDef>,
100    /// Top-level fields in spec order, components expanded in place
101    /// (groups appear as their counter tag).
102    pub field_order: Vec<Tag>,
103}
104
105#[derive(Debug, Clone, Default)]
106pub struct DataDictionary {
107    pub begin_string: String,
108    pub fields_by_tag: HashMap<Tag, FieldDef>,
109    pub tags_by_name: HashMap<String, Tag>,
110    pub header_tags: HashSet<Tag>,
111    pub header_required: Vec<Tag>,
112    pub trailer_tags: HashSet<Tag>,
113    pub trailer_required: Vec<Tag>,
114    pub messages: HashMap<String, MessageDef>,
115}
116
117/// Validation toggles (classic QuickFIX setting names).
118#[derive(Debug, Clone)]
119pub struct ValidationSettings {
120    pub check_fields_out_of_order: bool,
121    pub check_fields_have_values: bool,
122    pub check_user_defined_fields: bool,
123    pub allow_unknown_message_fields: bool,
124}
125
126impl Default for ValidationSettings {
127    fn default() -> Self {
128        Self {
129            check_fields_out_of_order: true,
130            check_fields_have_values: true,
131            check_user_defined_fields: true,
132            allow_unknown_message_fields: false,
133        }
134    }
135}
136
137// ----- XML loading -----
138
139/// Minimal DOM for the dictionary document.
140struct Node {
141    name: String,
142    attrs: HashMap<String, String>,
143    children: Vec<Node>,
144}
145
146fn parse_xml(text: &str) -> Result<Node> {
147    let mut reader = quick_xml::Reader::from_str(text);
148    reader.config_mut().trim_text(true);
149    let mut stack: Vec<Node> = vec![Node {
150        name: "(root)".into(),
151        attrs: HashMap::new(),
152        children: Vec::new(),
153    }];
154
155    let read_node = |e: &quick_xml::events::BytesStart<'_>| -> Result<Node> {
156        let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
157        let mut attrs = HashMap::new();
158        for attr in e.attributes() {
159            let attr = attr.map_err(|e| Error::Dictionary(format!("bad attribute: {e}")))?;
160            attrs.insert(
161                String::from_utf8_lossy(attr.key.as_ref()).into_owned(),
162                String::from_utf8_lossy(&attr.value).into_owned(),
163            );
164        }
165        Ok(Node { name, attrs, children: Vec::new() })
166    };
167
168    loop {
169        match reader.read_event() {
170            Ok(Event::Start(e)) => stack.push(read_node(&e)?),
171            Ok(Event::Empty(e)) => {
172                let node = read_node(&e)?;
173                stack.last_mut().unwrap().children.push(node);
174            }
175            Ok(Event::End(_)) => {
176                let node = stack.pop().unwrap();
177                stack
178                    .last_mut()
179                    .ok_or_else(|| Error::Dictionary("unbalanced XML".into()))?
180                    .children
181                    .push(node);
182            }
183            Ok(Event::Eof) => break,
184            Ok(_) => {}
185            Err(e) => return Err(Error::Dictionary(format!("XML parse error: {e}"))),
186        }
187    }
188    let mut root = stack.pop().ok_or_else(|| Error::Dictionary("empty document".into()))?;
189    root.children
190        .drain(..)
191        .find(|n| n.name == "fix")
192        .ok_or_else(|| Error::Dictionary("no <fix> root element".into()))
193}
194
195impl DataDictionary {
196    pub fn parse(text: &str) -> Result<Self> {
197        let fix = parse_xml(text)?;
198        let major = fix.attrs.get("major").cloned().unwrap_or_default();
199        let minor = fix.attrs.get("minor").cloned().unwrap_or_default();
200        let fixt = fix.attrs.get("type").map(|t| t == "FIXT").unwrap_or(false);
201        let mut dd = DataDictionary {
202            begin_string: if fixt {
203                format!("FIXT.{major}.{minor}")
204            } else {
205                format!("FIX.{major}.{minor}")
206            },
207            ..Default::default()
208        };
209        // Pre-FIX.4.2 dictionaries type most fields as CHAR meaning "string";
210        // single-character CHAR semantics only exist from 4.2 on.
211        let char_is_string = !fixt && dd.begin_string.as_str() < "FIX.4.2";
212
213        // Pass 1: field definitions (name -> tag/type/enums).
214        let fields = child(&fix, "fields");
215        if let Some(fields) = fields {
216            for f in fields.children.iter().filter(|c| c.name == "field") {
217                let tag: Tag = f
218                    .attrs
219                    .get("number")
220                    .and_then(|n| n.parse().ok())
221                    .ok_or_else(|| Error::Dictionary("field without number".into()))?;
222                let name = f.attrs.get("name").cloned().unwrap_or_default();
223                let mut field_type =
224                    FieldType::from_name(f.attrs.get("type").map(|s| s.as_str()).unwrap_or(""));
225                if char_is_string && field_type == FieldType::Char {
226                    field_type = FieldType::String;
227                }
228                let enum_values: Vec<(String, String)> = f
229                    .children
230                    .iter()
231                    .filter(|c| c.name == "value")
232                    .filter_map(|c| {
233                        c.attrs.get("enum").map(|v| {
234                            (v.clone(), c.attrs.get("description").cloned().unwrap_or_default())
235                        })
236                    })
237                    .collect();
238                let values = enum_values.iter().map(|(v, _)| v.clone()).collect();
239                dd.tags_by_name.insert(name.clone(), tag);
240                dd.fields_by_tag
241                    .insert(tag, FieldDef { tag, name, field_type, values, enum_values });
242            }
243        }
244
245        // Component definitions, unexpanded.
246        let mut components: HashMap<&str, &Node> = HashMap::new();
247        if let Some(comps) = child(&fix, "components") {
248            for c in comps.children.iter().filter(|c| c.name == "component") {
249                if let Some(name) = c.attrs.get("name") {
250                    components.insert(name, c);
251                }
252            }
253        }
254
255        // Header / trailer.
256        if let Some(header) = child(&fix, "header") {
257            let mut def = MessageDef::default();
258            dd.collect_message(header, &components, &mut def)?;
259            dd.header_tags = def.tags;
260            dd.header_required = def.required;
261        }
262        if let Some(trailer) = child(&fix, "trailer") {
263            let mut def = MessageDef::default();
264            dd.collect_message(trailer, &components, &mut def)?;
265            dd.trailer_tags = def.tags;
266            dd.trailer_required = def.required;
267        }
268
269        // Messages.
270        if let Some(messages) = child(&fix, "messages") {
271            for m in messages.children.iter().filter(|c| c.name == "message") {
272                let mut def = MessageDef {
273                    name: m.attrs.get("name").cloned().unwrap_or_default(),
274                    msg_type: m
275                        .attrs
276                        .get("msgtype")
277                        .cloned()
278                        .ok_or_else(|| Error::Dictionary("message without msgtype".into()))?,
279                    ..Default::default()
280                };
281                dd.collect_message(m, &components, &mut def)?;
282                dd.messages.insert(def.msg_type.clone(), def);
283            }
284        }
285        Ok(dd)
286    }
287
288    pub async fn load(path: impl AsRef<std::path::Path>) -> Result<Self> {
289        let text = tokio::fs::read_to_string(path).await?;
290        Self::parse(&text)
291    }
292
293    /// Recursively collect fields/components/groups of a message-like node.
294    fn collect_message(
295        &self,
296        node: &Node,
297        components: &HashMap<&str, &Node>,
298        def: &mut MessageDef,
299    ) -> Result<()> {
300        // Split borrows for the recursive walk.
301        let MessageDef { tags, required, groups, field_order, .. } = def;
302        self.collect(node, components, tags, required, groups, field_order)
303    }
304
305    fn collect(
306        &self,
307        node: &Node,
308        components: &HashMap<&str, &Node>,
309        tags_out: &mut HashSet<Tag>,
310        required_out: &mut Vec<Tag>,
311        groups_out: &mut HashMap<Tag, GroupDef>,
312        order_out: &mut Vec<Tag>,
313    ) -> Result<()> {
314        for c in &node.children {
315            let required = c.attrs.get("required").map(|r| r == "Y").unwrap_or(false);
316            match c.name.as_str() {
317                "field" => {
318                    let name = c.attrs.get("name").cloned().unwrap_or_default();
319                    let tag = *self
320                        .tags_by_name
321                        .get(&name)
322                        .ok_or_else(|| Error::Dictionary(format!("unknown field {name}")))?;
323                    if tags_out.insert(tag) {
324                        order_out.push(tag);
325                    }
326                    if required {
327                        required_out.push(tag);
328                    }
329                }
330                "component" => {
331                    let name = c.attrs.get("name").cloned().unwrap_or_default();
332                    let def = components
333                        .get(name.as_str())
334                        .ok_or_else(|| Error::Dictionary(format!("unknown component {name}")))?;
335                    // Component fields are required only if the component is.
336                    let mut comp_required = Vec::new();
337                    self.collect(def, components, tags_out, &mut comp_required, groups_out, order_out)?;
338                    if required {
339                        required_out.extend(comp_required);
340                    }
341                }
342                "group" => {
343                    let name = c.attrs.get("name").cloned().unwrap_or_default();
344                    let counter = *self
345                        .tags_by_name
346                        .get(&name)
347                        .ok_or_else(|| Error::Dictionary(format!("unknown group {name}")))?;
348                    if tags_out.insert(counter) {
349                        order_out.push(counter);
350                    }
351                    if required {
352                        required_out.push(counter);
353                    }
354                    let mut g = GroupDef { counter, ..Default::default() };
355                    let mut member_order: Vec<Tag> = Vec::new();
356                    self.collect_group(c, components, &mut g, &mut member_order)?;
357                    g.delimiter = *member_order
358                        .first()
359                        .ok_or_else(|| Error::Dictionary(format!("empty group {name}")))?;
360                    g.member_order = member_order;
361                    // Group member tags also count as "in message" for
362                    // tag-allowed checks.
363                    tags_out.extend(g.tags.iter().copied());
364                    groups_out.insert(counter, g);
365                }
366                _ => {}
367            }
368        }
369        Ok(())
370    }
371
372    fn collect_group(
373        &self,
374        node: &Node,
375        components: &HashMap<&str, &Node>,
376        group: &mut GroupDef,
377        member_order: &mut Vec<Tag>,
378    ) -> Result<()> {
379        for c in &node.children {
380            let required = c.attrs.get("required").map(|r| r == "Y").unwrap_or(false);
381            match c.name.as_str() {
382                "field" => {
383                    let name = c.attrs.get("name").cloned().unwrap_or_default();
384                    let tag = *self
385                        .tags_by_name
386                        .get(&name)
387                        .ok_or_else(|| Error::Dictionary(format!("unknown field {name}")))?;
388                    group.tags.insert(tag);
389                    member_order.push(tag);
390                    if required {
391                        group.required.push(tag);
392                    }
393                }
394                "component" => {
395                    let name = c.attrs.get("name").cloned().unwrap_or_default();
396                    let def = components
397                        .get(name.as_str())
398                        .ok_or_else(|| Error::Dictionary(format!("unknown component {name}")))?;
399                    self.collect_group(def, components, group, member_order)?;
400                }
401                "group" => {
402                    let name = c.attrs.get("name").cloned().unwrap_or_default();
403                    let counter = *self
404                        .tags_by_name
405                        .get(&name)
406                        .ok_or_else(|| Error::Dictionary(format!("unknown group {name}")))?;
407                    group.tags.insert(counter);
408                    member_order.push(counter);
409                    if required {
410                        group.required.push(counter);
411                    }
412                    let mut nested = GroupDef { counter, ..Default::default() };
413                    let mut nested_order = Vec::new();
414                    self.collect_group(c, components, &mut nested, &mut nested_order)?;
415                    nested.delimiter = *nested_order
416                        .first()
417                        .ok_or_else(|| Error::Dictionary(format!("empty group {name}")))?;
418                    nested.member_order = nested_order;
419                    group.tags.extend(nested.tags.iter().copied());
420                    group.groups.insert(counter, nested);
421                }
422                _ => {}
423            }
424        }
425        Ok(())
426    }
427
428    /// Combine a FIXT transport dictionary (header/trailer/admin messages)
429    /// with an application dictionary (app messages and fields) into one
430    /// effective dictionary for validation.
431    pub fn merged_with_app(mut self, app: &DataDictionary) -> DataDictionary {
432        for (tag, field) in &app.fields_by_tag {
433            self.fields_by_tag.entry(*tag).or_insert_with(|| field.clone());
434        }
435        for (name, tag) in &app.tags_by_name {
436            self.tags_by_name.entry(name.clone()).or_insert(*tag);
437        }
438        for (msg_type, def) in &app.messages {
439            self.messages.entry(msg_type.clone()).or_insert_with(|| def.clone());
440        }
441        self
442    }
443
444    /// Reorder a message body into the canonical form the reference engines
445    /// produce: top-level fields ascending by tag, with each repeating-group
446    /// block kept intact (in received/insertion order) under its counter tag.
447    pub fn canonicalize_body(&self, msg: &mut Message) {
448        let Ok(mt) = msg.msg_type() else { return };
449        let Some(def) = self.messages.get(&mt) else { return };
450        let fields = msg.body.take_fields();
451
452        let mut segments: Vec<(Tag, Vec<crate::field_map::TagValue>)> = Vec::new();
453        let mut i = 0;
454        while i < fields.len() {
455            let tag = fields[i].tag;
456            let mut seg = vec![fields[i].clone()];
457            i += 1;
458            if let Some(group) = def.groups.get(&tag) {
459                while i < fields.len() && group.tags.contains(&fields[i].tag) {
460                    seg.push(fields[i].clone());
461                    i += 1;
462                }
463            }
464            segments.push((tag, seg));
465        }
466        segments.sort_by_key(|s| s.0);
467        msg.body.set_fields(segments.into_iter().flat_map(|(_, seg)| seg).collect());
468    }
469
470    /// A ready-made [`GroupTemplate`] for reading a repeating group of this
471    /// message type (top-level groups only).
472    pub fn group_template(&self, msg_type: &str, counter: Tag) -> Option<GroupTemplate> {
473        let g = self.messages.get(msg_type)?.groups.get(&counter)?;
474        let mut members = vec![g.delimiter];
475        members.extend(g.tags.iter().copied().filter(|&t| t != g.delimiter));
476        Some(GroupTemplate::new(counter, members))
477    }
478
479    // ----- validation -----
480
481    /// Validate a parsed message. Returns the reject that should be sent
482    /// when it fails.
483    pub fn validate(
484        &self,
485        msg: &Message,
486        settings: &ValidationSettings,
487    ) -> std::result::Result<(), RejectError> {
488        let msg_type = msg
489            .header
490            .get_string(tags::MSG_TYPE)
491            .map_err(|_| RejectError::with_tag(SessionRejectReason::RequiredTagMissing, tags::MSG_TYPE))?;
492        // XMLnonFIX (35=n) is accepted without a message definition,
493        // matching QuickFIX/n.
494        if msg_type == "n" {
495            return Ok(());
496        }
497        let def = self.messages.get(&msg_type).ok_or_else(|| {
498            // Only FIX.4.2 cites RefTagID=35 on an invalid MsgType.
499            if self.begin_string == "FIX.4.2" {
500                RejectError::with_tag(SessionRejectReason::InvalidMsgType, tags::MSG_TYPE)
501            } else {
502                RejectError::new(SessionRejectReason::InvalidMsgType)
503            }
504        })?;
505
506        if settings.check_fields_out_of_order {
507            if let Some(tag) = msg.structure_error() {
508                return Err(RejectError::with_tag(
509                    SessionRejectReason::TagSpecifiedOutOfRequiredOrder,
510                    tag,
511                ));
512            }
513        }
514
515        // A repeating group's first entry must begin with its delimiter.
516        // Groups are checked in wire order so the first violation wins.
517        let body_fields: Vec<_> = msg.body.iter().collect();
518        for (i, f) in body_fields.iter().enumerate() {
519            let Some(group) = def.groups.get(&f.tag) else { continue };
520            let declared: u64 = msg.body.get_opt(group.counter).ok().flatten().unwrap_or(0);
521            if declared == 0 {
522                continue;
523            }
524            if let Some(first) = body_fields.get(i + 1) {
525                if group.tags.contains(&first.tag) && first.tag != group.delimiter {
526                    return Err(RejectError::other(
527                        format!(
528                            "Group {}'s first entry does not start with delimiter {}",
529                            group.counter, group.delimiter
530                        ),
531                        group.counter,
532                    ));
533                }
534            }
535        }
536
537        // Required fields.
538        for &tag in &self.header_required {
539            if !msg.header.contains(tag) {
540                return Err(RejectError::with_tag(SessionRejectReason::RequiredTagMissing, tag));
541            }
542        }
543        for &tag in &self.trailer_required {
544            if !msg.trailer.contains(tag) {
545                return Err(RejectError::with_tag(SessionRejectReason::RequiredTagMissing, tag));
546            }
547        }
548        for &tag in &def.required {
549            if !msg.body.contains(tag) {
550                return Err(RejectError::with_tag(SessionRejectReason::RequiredTagMissing, tag));
551            }
552        }
553
554        // Duplicate tags: only repeating-group members may repeat.
555        let mut group_member_tags: HashSet<Tag> = HashSet::new();
556        for g in def.groups.values() {
557            group_member_tags.extend(g.tags.iter().copied());
558        }
559        let mut seen: HashSet<Tag> = HashSet::new();
560        for f in msg.body.iter() {
561            if !group_member_tags.contains(&f.tag) && !seen.insert(f.tag) {
562                return Err(RejectError::with_tag(
563                    SessionRejectReason::TagAppearsMoreThanOnce,
564                    f.tag,
565                ));
566            }
567        }
568
569        // Group counts (declared NumInGroup vs actual delimiter count) —
570        // checked before per-field content so a count mismatch wins over
571        // value errors inside the group.
572        for group in def.groups.values() {
573            if let Some(raw) = msg.body.get_raw(group.counter) {
574                let declared: usize = std::str::from_utf8(raw)
575                    .ok()
576                    .and_then(|s| s.parse().ok())
577                    .ok_or_else(|| {
578                        RejectError::with_tag(
579                            SessionRejectReason::IncorrectDataFormatForValue,
580                            group.counter,
581                        )
582                    })?;
583                let actual =
584                    msg.body.iter().filter(|f| f.tag == group.delimiter).count();
585                if declared != actual {
586                    return Err(RejectError::with_tag(
587                        SessionRejectReason::IncorrectNumInGroupCountForRepeatingGroup,
588                        group.counter,
589                    ));
590                }
591            }
592        }
593
594        // Per-field checks.
595        for section in [&msg.header, &msg.body, &msg.trailer] {
596            let is_body = std::ptr::eq(section, &msg.body);
597            for f in section.iter() {
598                self.check_field(f.tag, &f.value, is_body, def, settings)?;
599            }
600        }
601        Ok(())
602    }
603
604    fn check_field(
605        &self,
606        tag: Tag,
607        value: &[u8],
608        is_body: bool,
609        def: &MessageDef,
610        settings: &ValidationSettings,
611    ) -> std::result::Result<(), RejectError> {
612        if tag >= USER_DEFINED_TAG_MIN && !settings.check_user_defined_fields {
613            return Ok(());
614        }
615        if settings.check_fields_have_values && value.is_empty() {
616            return Err(RejectError::with_tag(
617                SessionRejectReason::TagSpecifiedWithoutAValue,
618                tag,
619            ));
620        }
621        let Some(field) = self.fields_by_tag.get(&tag) else {
622            // User-defined tags were already skipped above when
623            // ValidateUserDefinedFields=N; here an unknown tag is an error
624            // unless unknown fields are allowed outright.
625            if settings.allow_unknown_message_fields {
626                return Ok(());
627            }
628            return Err(RejectError::with_tag(SessionRejectReason::InvalidTagNumber, tag));
629        };
630        if is_body
631            && !settings.allow_unknown_message_fields
632            && !def.tags.contains(&tag)
633            && tag < USER_DEFINED_TAG_MIN
634        {
635            return Err(RejectError::with_tag(
636                SessionRejectReason::TagNotDefinedForThisMessageType,
637                tag,
638            ));
639        }
640        // Format before enum membership: a malformed value is "incorrect
641        // data format" (373=6), not "value out of range" (373=5).
642        self.check_format(field, value).map_err(|_| {
643            RejectError::with_tag(SessionRejectReason::IncorrectDataFormatForValue, tag)
644        })?;
645        if !field.values.is_empty() {
646            let v = String::from_utf8_lossy(value);
647            // MultipleValue fields carry space-separated entries.
648            let ok = v.split(' ').all(|part| field.values.contains(part));
649            if !ok {
650                return Err(RejectError::with_tag(SessionRejectReason::ValueIsIncorrect, tag));
651            }
652        }
653        Ok(())
654    }
655
656    fn check_format(&self, field: &FieldDef, value: &[u8]) -> std::result::Result<(), ()> {
657        use crate::value::{FixDate, FixDecode, UtcTimestamp};
658        let ok = match field.field_type {
659            FieldType::Int => i64::decode(field.tag, value).is_ok(),
660            FieldType::Length | FieldType::SeqNum | FieldType::NumInGroup => {
661                u64::decode(field.tag, value).is_ok()
662            }
663            FieldType::DayOfMonth => {
664                u64::decode(field.tag, value).map(|d| (1..=31).contains(&d)).unwrap_or(false)
665            }
666            FieldType::Float
667            | FieldType::Qty
668            | FieldType::Price
669            | FieldType::PriceOffset
670            | FieldType::Amt
671            | FieldType::Percentage => f64::decode(field.tag, value).is_ok(),
672            FieldType::Char => value.len() == 1,
673            FieldType::Boolean => matches!(value, b"Y" | b"N"),
674            FieldType::UtcTimestamp => UtcTimestamp::decode(field.tag, value).is_ok(),
675            FieldType::UtcDateOnly | FieldType::LocalMktDate => {
676                FixDate::decode(field.tag, value).is_ok()
677            }
678            FieldType::MonthYear => {
679                value.len() >= 6 && value[..6].iter().all(|b| b.is_ascii_digit())
680            }
681            FieldType::UtcTimeOnly | FieldType::String | FieldType::Data | FieldType::Other => {
682                true
683            }
684        };
685        if ok { Ok(()) } else { Err(()) }
686    }
687}
688
689fn child<'a>(node: &'a Node, name: &str) -> Option<&'a Node> {
690    node.children.iter().find(|c| c.name == name)
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    fn fix44() -> DataDictionary {
698        let text = std::fs::read_to_string(concat!(
699            env!("CARGO_MANIFEST_DIR"),
700            "/spec/FIX44.xml"
701        ))
702        .unwrap();
703        DataDictionary::parse(&text).unwrap()
704    }
705
706    fn valid_order() -> Message {
707        let mut m = Message::with_type("D");
708        m.header.set(tags::BEGIN_STRING, "FIX.4.4");
709        m.header.set(tags::SENDER_COMP_ID, "A");
710        m.header.set(tags::TARGET_COMP_ID, "B");
711        m.header.set(tags::MSG_SEQ_NUM, 2u64);
712        m.stamp_sending_time(crate::value::UtcTimestamp::now());
713        m.set(11, "ORDER-1");
714        m.set(55, "TSLA");
715        m.set(54, '1');
716        m.set(60, crate::value::UtcTimestamp::now());
717        m.set(40, '1');
718        m
719    }
720
721    #[test]
722    fn loads_fix44_spec() {
723        let dd = fix44();
724        assert_eq!(dd.begin_string, "FIX.4.4");
725        assert!(dd.messages.contains_key("D"), "NewOrderSingle should exist");
726        assert!(dd.header_tags.contains(&tags::MSG_SEQ_NUM));
727        assert_eq!(dd.fields_by_tag[&54].field_type, FieldType::Char);
728        assert!(dd.fields_by_tag[&54].values.contains("1"));
729        // NewOrderSingle has the NoPartyIDs group via the Parties component.
730        let d = &dd.messages["D"];
731        assert!(d.groups.contains_key(&453), "NoPartyIDs group expected");
732        assert_eq!(d.groups[&453].delimiter, 448);
733    }
734
735    #[test]
736    fn validates_good_message() {
737        let dd = fix44();
738        let msg =
739            Message::parse(&valid_order().to_bytes(), true).expect("roundtrip");
740        dd.validate(&msg, &ValidationSettings::default()).expect("should validate");
741    }
742
743    #[test]
744    fn missing_required_field_rejected() {
745        let dd = fix44();
746        let mut order = valid_order();
747        order.body.remove(11); // ClOrdID is required='Y' in NewOrderSingle
748        let msg = Message::parse(&order.to_bytes(), true).unwrap();
749        let err = dd.validate(&msg, &ValidationSettings::default()).unwrap_err();
750        assert_eq!(err.reason, SessionRejectReason::RequiredTagMissing);
751        assert_eq!(err.ref_tag, Some(11));
752    }
753
754    #[test]
755    fn bad_enum_value_rejected() {
756        let dd = fix44();
757        let mut order = valid_order();
758        order.set(54, 'Z'); // not a valid Side
759        let msg = Message::parse(&order.to_bytes(), true).unwrap();
760        let err = dd.validate(&msg, &ValidationSettings::default()).unwrap_err();
761        assert_eq!(err.reason, SessionRejectReason::ValueIsIncorrect);
762        assert_eq!(err.ref_tag, Some(54));
763    }
764
765    #[test]
766    fn undefined_tag_rejected() {
767        let dd = fix44();
768        let mut order = valid_order();
769        order.set(4999, "bogus"); // not defined in FIX44
770        let msg = Message::parse(&order.to_bytes(), true).unwrap();
771        let err = dd.validate(&msg, &ValidationSettings::default()).unwrap_err();
772        assert_eq!(err.reason, SessionRejectReason::InvalidTagNumber);
773    }
774
775    #[test]
776    fn tag_not_defined_for_message_type() {
777        let dd = fix44();
778        let mut order = valid_order();
779        order.set(112, "TR1"); // TestReqID doesn't belong in NewOrderSingle
780        let msg = Message::parse(&order.to_bytes(), true).unwrap();
781        let err = dd.validate(&msg, &ValidationSettings::default()).unwrap_err();
782        assert_eq!(err.reason, SessionRejectReason::TagNotDefinedForThisMessageType);
783        assert_eq!(err.ref_tag, Some(112));
784    }
785
786    #[test]
787    fn wrong_group_count_rejected() {
788        let dd = fix44();
789        let mut order = valid_order();
790        order.set(453, 2u32); // declare two parties...
791        order.body.push(448, "PARTY-A"); // ...but provide one
792        order.body.push(447, 'D');
793        order.body.push(452, 1u32);
794        let msg = Message::parse(&order.to_bytes(), true).unwrap();
795        let err = dd.validate(&msg, &ValidationSettings::default()).unwrap_err();
796        assert_eq!(
797            err.reason,
798            SessionRejectReason::IncorrectNumInGroupCountForRepeatingGroup
799        );
800    }
801
802    #[test]
803    fn unknown_msg_type_rejected() {
804        let dd = fix44();
805        let mut m = valid_order();
806        m.header.set(tags::MSG_TYPE, "ZZ");
807        let msg = Message::parse(&m.to_bytes(), true).unwrap();
808        let err = dd.validate(&msg, &ValidationSettings::default()).unwrap_err();
809        assert_eq!(err.reason, SessionRejectReason::InvalidMsgType);
810    }
811
812    #[test]
813    fn group_template_reads_parties() {
814        let dd = fix44();
815        let tpl = dd.group_template("D", 453).unwrap();
816        assert_eq!(tpl.num_tag, 453);
817        assert_eq!(tpl.delimiter(), 448);
818
819        let mut order = valid_order();
820        let mut g1 = crate::field_map::FieldMap::new();
821        g1.push(448, "PARTY-A");
822        g1.push(447, 'D');
823        let mut g2 = crate::field_map::FieldMap::new();
824        g2.push(448, "PARTY-B");
825        g2.push(447, 'D');
826        order.body.write_groups(&tpl, &[g1, g2]);
827
828        let msg = Message::parse(&order.to_bytes(), true).unwrap();
829        dd.validate(&msg, &ValidationSettings::default()).expect("groups valid");
830        let groups = msg.body.read_groups(&tpl).unwrap();
831        assert_eq!(groups.len(), 2);
832        assert_eq!(groups[1].get_string(448).unwrap(), "PARTY-B");
833    }
834}