hotfix_dictionary/
lib.rs

1//! Access to FIX Dictionary reference and message specifications.
2
3mod quickfix;
4
5pub use datatype::FixDatatype;
6use fnv::FnvHashMap;
7use quickfix::{ParseDictionaryError, QuickFixReader};
8use smartstring::alias::String as SmartString;
9use std::{fmt, sync::Arc};
10
11/// Type alias for FIX tags: 32-bit unsigned integers, strictly positive.
12pub type TagU32 = std::num::NonZeroU32;
13
14pub trait DataFieldLookup<F> {
15    fn field_is_data(&self, field: F) -> bool;
16}
17
18pub trait NumInGroupLookup<F> {
19    fn field_is_num_in_group(&self, field: F) -> bool;
20}
21
22impl DataFieldLookup<u32> for Dictionary {
23    fn field_is_data(&self, tag: u32) -> bool {
24        if let Some(field) = self.field_by_tag(tag) {
25            field.data_type().basetype() == FixDatatype::Data
26        } else {
27            false
28        }
29    }
30}
31
32impl NumInGroupLookup<u32> for Dictionary {
33    fn field_is_num_in_group(&self, tag: u32) -> bool {
34        if let Some(field) = self.field_by_tag(tag) {
35            field.data_type().basetype() == FixDatatype::NumInGroup
36        } else {
37            false
38        }
39    }
40}
41
42/// The expected location of a field within a FIX message (i.e. header, body, or
43/// trailer).
44#[derive(Debug, Copy, Clone, PartialEq, Eq)]
45pub enum FieldLocation {
46    /// The field is located inside the "Standard Header".
47    Header,
48    /// This field is located inside the body of the FIX message.
49    Body,
50    /// This field is located inside the "Standard Trailer".
51    Trailer,
52}
53
54/// A mapping from FIX version strings to [`Dictionary`] values.
55pub type Dictionaries = FnvHashMap<String, Arc<Dictionary>>;
56
57/// Specifies business semantics for application-level entities within the FIX
58/// Protocol.
59///
60/// You can rely on [`Dictionary`] for accessing details about
61/// fields, messages, and other abstract entities as defined in the FIX
62/// specifications. Examples of such information include:
63///
64/// - The mapping of FIX field names to numeric tags (e.g. `BeginString` is 8).
65/// - Which FIX fields are mandatory and which are optional.
66/// - The data type of each and every FIX field.
67/// - What fields to expect in FIX headers.
68///
69/// N.B. The FIX Protocol mandates separation of concerns between session and
70/// application protocol only for FIX 5.0 and subsequent versions. All FIX
71/// Dictionaries for older versions will also contain information about the
72/// session layer.
73#[derive(Debug, Clone)]
74pub struct Dictionary {
75    version: String,
76
77    abbreviation_definitions: FnvHashMap<SmartString, AbbreviationData>,
78
79    data_types_by_name: FnvHashMap<SmartString, DatatypeData>,
80
81    fields_by_tags: FnvHashMap<u32, FieldData>,
82    field_tags_by_name: FnvHashMap<SmartString, u32>,
83
84    components_by_name: FnvHashMap<SmartString, ComponentData>,
85
86    messages_by_msgtype: FnvHashMap<SmartString, MessageData>,
87    message_msgtypes_by_name: FnvHashMap<SmartString, SmartString>,
88
89    //layout_items: Vec<LayoutItemData>,
90    categories_by_name: FnvHashMap<SmartString, CategoryData>,
91}
92
93#[allow(dead_code)]
94fn display_layout_item(indent: u32, item: LayoutItem, f: &mut fmt::Formatter) -> fmt::Result {
95    for _ in 0..indent {
96        write!(f, " ")?;
97    }
98    match item.kind() {
99        LayoutItemKind::Field(_) => {
100            writeln!(
101                f,
102                "<field name='{}' required='{}' />",
103                item.tag_text(),
104                item.required(),
105            )?;
106        }
107        LayoutItemKind::Group(_, _fields) => {
108            writeln!(
109                f,
110                "<group name='{}' required='{}' />",
111                item.tag_text(),
112                item.required(),
113            )?;
114            writeln!(f, "</group>")?;
115        }
116        LayoutItemKind::Component(_c) => {
117            writeln!(
118                f,
119                "<component name='{}' required='{}' />",
120                item.tag_text(),
121                item.required(),
122            )?;
123            writeln!(f, "</component>")?;
124        }
125    }
126    Ok(())
127}
128
129impl Dictionary {
130    /// Creates a new empty FIX Dictionary named `version`.
131    fn new<S: ToString>(version: S) -> Self {
132        Dictionary {
133            version: version.to_string(),
134            abbreviation_definitions: FnvHashMap::default(),
135            data_types_by_name: FnvHashMap::default(),
136            fields_by_tags: FnvHashMap::default(),
137            field_tags_by_name: FnvHashMap::default(),
138            components_by_name: FnvHashMap::default(),
139            messages_by_msgtype: FnvHashMap::default(),
140            message_msgtypes_by_name: FnvHashMap::default(),
141            categories_by_name: FnvHashMap::default(),
142        }
143    }
144
145    /// Attempts to read a QuickFIX-style specification file and convert it into
146    /// a [`Dictionary`].
147    pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseDictionaryError> {
148        let xml_document =
149            roxmltree::Document::parse(input).map_err(|_| ParseDictionaryError::InvalidFormat)?;
150        QuickFixReader::new(&xml_document)
151    }
152
153    /// Returns the version string associated with this [`Dictionary`] (e.g.
154    /// `FIXT.1.1`, `FIX.4.2`).
155    ///
156    /// ```
157    /// use hotfix_dictionary::Dictionary;
158    ///
159    /// let dict = Dictionary::fix44();
160    /// assert_eq!(dict.version(), "FIX.4.4");
161    /// ```
162    pub fn version(&self) -> &str {
163        self.version.as_str()
164    }
165
166    pub fn load_from_file(path: &str) -> Result<Self, ParseDictionaryError> {
167        let spec = std::fs::read_to_string(path).unwrap();
168        Dictionary::from_quickfix_spec(&spec)
169    }
170
171    /// Creates a new [`Dictionary`] for FIX 4.0.
172    #[cfg(feature = "fix40")]
173    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix40")))]
174    pub fn fix40() -> Self {
175        let spec = include_str!("resources/quickfix/FIX-4.0.xml");
176        Dictionary::from_quickfix_spec(spec).unwrap()
177    }
178
179    /// Creates a new [`Dictionary`] for FIX 4.1.
180    #[cfg(feature = "fix41")]
181    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix41")))]
182    pub fn fix41() -> Self {
183        let spec = include_str!("resources/quickfix/FIX-4.1.xml");
184        Dictionary::from_quickfix_spec(spec).unwrap()
185    }
186
187    /// Creates a new [`Dictionary`] for FIX 4.2.
188    #[cfg(feature = "fix42")]
189    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix42")))]
190    pub fn fix42() -> Self {
191        let spec = include_str!("resources/quickfix/FIX-4.2.xml");
192        Dictionary::from_quickfix_spec(spec).unwrap()
193    }
194
195    /// Creates a new [`Dictionary`] for FIX 4.3.
196    #[cfg(feature = "fix43")]
197    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix43")))]
198    pub fn fix43() -> Self {
199        let spec = include_str!("resources/quickfix/FIX-4.3.xml");
200        Dictionary::from_quickfix_spec(spec).unwrap()
201    }
202
203    /// Creates a new [`Dictionary`] for FIX 4.4.
204    pub fn fix44() -> Self {
205        let spec = include_str!("resources/quickfix/FIX-4.4.xml");
206        Dictionary::from_quickfix_spec(spec).unwrap()
207    }
208
209    /// Creates a new [`Dictionary`] for FIX 5.0.
210    #[cfg(feature = "fix50")]
211    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix50")))]
212    pub fn fix50() -> Self {
213        let spec = include_str!("resources/quickfix/FIX-5.0.xml");
214        Dictionary::from_quickfix_spec(spec).unwrap()
215    }
216
217    /// Creates a new [`Dictionary`] for FIX 5.0 SP1.
218    #[cfg(feature = "fix50sp1")]
219    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix50sp1")))]
220    pub fn fix50sp1() -> Self {
221        let spec = include_str!("resources/quickfix/FIX-5.0-SP1.xml");
222        Dictionary::from_quickfix_spec(spec).unwrap()
223    }
224
225    /// Creates a new [`Dictionary`] for FIX 5.0 SP2.
226    #[cfg(feature = "fix50sp2")]
227    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix50sp1")))]
228    pub fn fix50sp2() -> Self {
229        let spec = include_str!("resources/quickfix/FIX-5.0-SP2.xml");
230        Dictionary::from_quickfix_spec(spec).unwrap()
231    }
232
233    /// Creates a new [`Dictionary`] for FIXT 1.1.
234    #[cfg(feature = "fixt11")]
235    #[cfg_attr(doc_cfg, doc(cfg(feature = "fixt11")))]
236    pub fn fixt11() -> Self {
237        let spec = include_str!("resources/quickfix/FIXT-1.1.xml");
238        Dictionary::from_quickfix_spec(spec).unwrap()
239    }
240
241    /// Returns a [`Vec`] of FIX [`Dictionary`]'s for the most common FIX
242    /// versions (that have been enabled via feature flags). This is only
243    /// intended for testing purposes.
244    pub fn common_dictionaries() -> Vec<Dictionary> {
245        vec![
246            #[cfg(feature = "fix40")]
247            Self::fix40(),
248            #[cfg(feature = "fix41")]
249            Self::fix41(),
250            #[cfg(feature = "fix42")]
251            Self::fix42(),
252            #[cfg(feature = "fix43")]
253            Self::fix43(),
254            Self::fix44(),
255            #[cfg(feature = "fix50")]
256            Self::fix50(),
257            #[cfg(feature = "fix50sp1")]
258            Self::fix50sp1(),
259            #[cfg(feature = "fix50sp2")]
260            Self::fix50sp2(),
261            #[cfg(feature = "fixt11")]
262            Self::fixt11(),
263        ]
264    }
265
266    /// Return the known abbreviation for `term` -if any- according to the
267    /// documentation of this FIX Dictionary.
268    pub fn abbreviation_for(&self, term: &str) -> Option<Abbreviation<'_>> {
269        self.abbreviation_definitions
270            .get(term)
271            .map(|data| Abbreviation(self, data))
272    }
273
274    /// Returns the [`Message`] associated with `name`, if any.
275    ///
276    /// ```
277    /// use hotfix_dictionary::Dictionary;
278    ///
279    /// let dict = Dictionary::fix44();
280    ///
281    /// let msg1 = dict.message_by_name("Heartbeat").unwrap();
282    /// let msg2 = dict.message_by_msgtype("0").unwrap();
283    /// assert_eq!(msg1.name(), msg2.name());
284    /// ```
285    pub fn message_by_name(&self, name: &str) -> Option<Message<'_>> {
286        let msg_type = self.message_msgtypes_by_name.get(name)?;
287        self.message_by_msgtype(msg_type)
288    }
289
290    /// Returns the [`Message`] that has the given `msgtype`, if any.
291    ///
292    /// ```
293    /// use hotfix_dictionary::Dictionary;
294    ///
295    /// let dict = Dictionary::fix44();
296    ///
297    /// let msg1 = dict.message_by_msgtype("0").unwrap();
298    /// let msg2 = dict.message_by_name("Heartbeat").unwrap();
299    /// assert_eq!(msg1.name(), msg2.name());
300    /// ```
301    pub fn message_by_msgtype(&self, msgtype: &str) -> Option<Message<'_>> {
302        self.messages_by_msgtype
303            .get(msgtype)
304            .map(|data| Message(self, data))
305    }
306
307    /// Returns the [`Component`] named `name`, if any.
308    pub fn component_by_name(&self, name: &str) -> Option<Component<'_>> {
309        self.components_by_name
310            .get(name)
311            .map(|data| Component(self, data))
312    }
313
314    /// Returns the [`Datatype`] named `name`, if any.
315    ///
316    /// ```
317    /// use hotfix_dictionary::Dictionary;
318    ///
319    /// let dict = Dictionary::fix44();
320    /// let dt = dict.datatype_by_name("String").unwrap();
321    /// assert_eq!(dt.name(), "String");
322    /// ```
323    pub fn datatype_by_name(&self, name: &str) -> Option<Datatype<'_>> {
324        self.data_types_by_name
325            .get(name)
326            .map(|data| Datatype(self, data))
327    }
328
329    /// Returns the [`Field`] associated with `tag`, if any.
330    ///
331    /// ```
332    /// use hotfix_dictionary::Dictionary;
333    ///
334    /// let dict = Dictionary::fix44();
335    /// let field1 = dict.field_by_tag(112).unwrap();
336    /// let field2 = dict.field_by_name("TestReqID").unwrap();
337    /// assert_eq!(field1.name(), field2.name());
338    /// ```
339    pub fn field_by_tag(&self, tag: u32) -> Option<Field<'_>> {
340        self.fields_by_tags.get(&tag).map(|data| Field(self, data))
341    }
342
343    /// Returns the [`Field`] named `name`, if any.
344    pub fn field_by_name(&self, name: &str) -> Option<Field<'_>> {
345        let tag = self.field_tags_by_name.get(name)?;
346        self.field_by_tag(*tag)
347    }
348
349    /// Returns the [`Category`] named `name`, if any.
350    fn category_by_name(&self, name: &str) -> Option<Category<'_>> {
351        self.categories_by_name
352            .get(name)
353            .map(|data| Category(self, data))
354    }
355
356    /// Returns a [`Vec`] of all [`Datatype`]'s in this [`Dictionary`]. The ordering
357    /// of items is not specified.
358    ///
359    /// ```
360    /// use hotfix_dictionary::Dictionary;
361    ///
362    /// let dict = Dictionary::fix44();
363    /// // FIX 4.4 defines 23 (FIXME) datatypes.
364    /// assert_eq!(dict.datatypes().len(), 23);
365    /// ```
366    pub fn datatypes(&self) -> Vec<Datatype<'_>> {
367        self.data_types_by_name
368            .values()
369            .map(|data| Datatype(self, data))
370            .collect()
371    }
372
373    /// Returns a [`Vec`] of all [`Message`]'s in this [`Dictionary`]. The ordering
374    /// of items is not specified.
375    ///
376    /// ```
377    /// use hotfix_dictionary::Dictionary;
378    ///
379    /// let dict = Dictionary::fix44();
380    /// let msgs = dict.messages();
381    /// let msg = msgs.iter().find(|m| m.name() == "MarketDataRequest");
382    /// assert_eq!(msg.unwrap().msg_type(), "V");
383    /// ```
384    pub fn messages(&self) -> Vec<Message<'_>> {
385        self.messages_by_msgtype
386            .values()
387            .map(|data| Message(self, data))
388            .collect()
389    }
390
391    /// Returns a [`Vec`] of all [`Category`]'s in this [`Dictionary`]. The ordering
392    /// of items is not specified.
393    pub fn categories(&self) -> Vec<Category<'_>> {
394        self.categories_by_name
395            .values()
396            .map(|data| Category(self, data))
397            .collect()
398    }
399
400    /// Returns a [`Vec`] of all [`Field`]'s in this [`Dictionary`]. The ordering
401    /// of items is not specified.
402    pub fn fields(&self) -> Vec<Field<'_>> {
403        self.fields_by_tags
404            .values()
405            .map(|data| Field(self, data))
406            .collect()
407    }
408
409    /// Returns a [`Vec`] of all [`Component`]'s in this [`Dictionary`]. The ordering
410    /// of items is not specified.
411    pub fn components(&self) -> Vec<Component<'_>> {
412        self.components_by_name
413            .values()
414            .map(|data| Component(self, data))
415            .collect()
416    }
417}
418
419/// Builder utilities
420impl Dictionary {
421    fn add_field(&mut self, field: FieldData) {
422        self.field_tags_by_name
423            .insert(field.name.clone(), field.tag);
424        self.fields_by_tags.insert(field.tag, field);
425    }
426
427    fn add_message(&mut self, message: MessageData) {
428        self.message_msgtypes_by_name
429            .insert(message.name.clone(), message.msg_type.clone());
430        self.messages_by_msgtype
431            .insert(message.msg_type.clone(), message);
432    }
433
434    fn add_component(&mut self, component: ComponentData) {
435        self.components_by_name
436            .insert(component.name.clone(), component);
437    }
438
439    fn add_datatype(&mut self, datatype: DatatypeData) {
440        self.data_types_by_name
441            .insert(datatype.datatype.name().into(), datatype);
442    }
443
444    fn add_category(&mut self, category: CategoryData) {
445        self.categories_by_name
446            .insert(category.name.clone().into(), category);
447    }
448}
449
450#[derive(Clone, Debug)]
451struct AbbreviationData {
452    abbreviation: SmartString,
453}
454
455/// An [`Abbreviation`] is a standardized abbreviated form for a specific word,
456/// pattern, or name. Abbreviation data is mostly meant for documentation
457/// purposes, but in general it can have other uses as well, e.g. FIXML field
458/// naming.
459#[allow(dead_code)]
460#[derive(Debug)]
461pub struct Abbreviation<'a>(&'a Dictionary, &'a AbbreviationData);
462
463impl<'a> Abbreviation<'a> {
464    /// Returns the full term (non-abbreviated) associated with `self`.
465    pub fn term(&self) -> &str {
466        self.1.abbreviation.as_str()
467    }
468}
469
470#[derive(Clone, Debug)]
471struct CategoryData {
472    /// **Primary key**. A string uniquely identifying this category.
473    name: String,
474}
475
476/// A [`Category`] is a collection of loosely related FIX messages or components
477/// all belonging to the same [`Section`].
478#[derive(Clone, Debug)]
479#[allow(dead_code)]
480pub struct Category<'a>(&'a Dictionary, &'a CategoryData);
481
482#[derive(Clone, Debug)]
483struct ComponentData {
484    /// **Primary key.** The unique integer identifier of this component
485    /// type.
486    id: usize,
487    component_type: FixmlComponentAttributes,
488    layout_items: Vec<LayoutItemData>,
489    category_name: SmartString,
490    /// The human readable name of the component.
491    name: SmartString,
492}
493
494/// A [`Component`] is an ordered collection of fields and/or other components.
495/// There are two kinds of components: (1) common blocks and (2) repeating
496/// groups. Common blocks are merely commonly reused sequences of the same
497/// fields/components
498/// which are given names for simplicity, i.e. they serve as "macros". Repeating
499/// groups, on the other hand, are components which can appear zero or more times
500/// inside FIX messages (or other components, for that matter).
501#[derive(Clone, Debug)]
502pub struct Component<'a>(&'a Dictionary, &'a ComponentData);
503
504impl<'a> Component<'a> {
505    /// Returns the unique numberic ID of `self`.
506    pub fn id(&self) -> u32 {
507        self.1.id as u32
508    }
509
510    /// Returns the name of `self`. The name of every [`Component`] is unique
511    /// across a [`Dictionary`].
512    pub fn name(&self) -> &str {
513        self.1.name.as_str()
514    }
515
516    /// Returns `true` if and only if `self` is a "group" component; `false`
517    /// otherwise.
518    pub fn is_group(&self) -> bool {
519        match self.1.component_type {
520            FixmlComponentAttributes::Block { is_repeating, .. } => is_repeating,
521            _ => false,
522        }
523    }
524
525    /// Returns the [`Category`] to which `self` belongs.
526    pub fn category(&self) -> Category<'_> {
527        self.0
528            .category_by_name(self.1.category_name.as_str())
529            .unwrap()
530    }
531
532    /// Returns an [`Iterator`] over all items that are part of `self`.
533    pub fn items(&self) -> impl Iterator<Item = LayoutItem<'_>> {
534        self.1
535            .layout_items
536            .iter()
537            .map(move |data| LayoutItem(self.0, data))
538    }
539
540    /// Checks whether `field` appears in the definition of `self` and returns
541    /// `true` if it does, `false` otherwise.
542    pub fn contains_field(&self, field: &Field) -> bool {
543        self.items().any(|layout_item| {
544            if let LayoutItemKind::Field(f) = layout_item.kind() {
545                f.tag() == field.tag()
546            } else {
547                false
548            }
549        })
550    }
551}
552
553/// Component type (FIXML-specific information).
554#[derive(Clone, Debug, PartialEq)]
555#[allow(dead_code)]
556pub enum FixmlComponentAttributes {
557    Xml,
558    Block {
559        is_repeating: bool,
560        is_implicit: bool,
561        is_optimized: bool,
562    },
563    Message,
564}
565
566#[derive(Clone, Debug, PartialEq)]
567struct DatatypeData {
568    /// **Primary key.** Identifier of the datatype.
569    datatype: FixDatatype,
570    /// Human readable description of this Datatype.
571    description: String,
572    /// A string that contains examples values for a datatype
573    examples: Vec<String>,
574    // TODO: 'XML'.
575}
576
577/// A FIX data type defined as part of a [`Dictionary`].
578#[derive(Debug)]
579#[allow(dead_code)]
580pub struct Datatype<'a>(&'a Dictionary, &'a DatatypeData);
581
582impl<'a> Datatype<'a> {
583    /// Returns the name of `self`.  This is also guaranteed to be a valid Rust
584    /// identifier.
585    pub fn name(&self) -> &str {
586        self.1.datatype.name()
587    }
588
589    /// Returns `self` as an `enum`.
590    pub fn basetype(&self) -> FixDatatype {
591        self.1.datatype
592    }
593}
594
595mod datatype {
596    use strum::IntoEnumIterator;
597    use strum_macros::{EnumIter, IntoStaticStr};
598
599    /// Sum type for all possible FIX data types ever defined across all FIX
600    /// application versions.
601    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, EnumIter, IntoStaticStr)]
602    #[repr(u8)]
603    #[non_exhaustive]
604    pub enum FixDatatype {
605        /// Single character value, can include any alphanumeric character or
606        /// punctuation except the delimiter. All char fields are case sensitive
607        /// (i.e. m != M). The following fields are based on char.
608        Char,
609        /// char field containing one of two values: 'Y' = True/Yes 'N' = False/No.
610        Boolean,
611        /// Sequence of digits with optional decimal point and sign character (ASCII
612        /// characters "-", "0" - "9" and "."); the absence of the decimal point
613        /// within the string will be interpreted as the float representation of an
614        /// integer value. All float fields must accommodate up to fifteen
615        /// significant digits. The number of decimal places used should be a factor
616        /// of business/market needs and mutual agreement between counterparties.
617        /// Note that float values may contain leading zeros (e.g. "00023.23" =
618        /// "23.23") and may contain or omit trailing zeros after the decimal point
619        /// (e.g. "23.0" = "23.0000" = "23" = "23."). Note that fields which are
620        /// derived from float may contain negative values unless explicitly
621        /// specified otherwise. The following data types are based on float.
622        Float,
623        /// float field typically representing a Price times a Qty.
624        Amt,
625        /// float field representing a price. Note the number of decimal places may
626        /// vary. For certain asset classes prices may be negative values. For
627        /// example, prices for options strategies can be negative under certain
628        /// market conditions. Refer to Volume 7: FIX Usage by Product for asset
629        /// classes that support negative price values.
630        Price,
631        /// float field representing a price offset, which can be mathematically
632        /// added to a "Price". Note the number of decimal places may vary and some
633        /// fields such as LastForwardPoints may be negative.
634        PriceOffset,
635        /// float field capable of storing either a whole number (no decimal places)
636        /// of "shares" (securities denominated in whole units) or a decimal value
637        /// containing decimal places for non-share quantity asset classes
638        /// (securities denominated in fractional units).
639        Qty,
640        /// float field representing a percentage (e.g. 0.05 represents 5% and 0.9525
641        /// represents 95.25%). Note the number of decimal places may vary.
642        Percentage,
643        /// Sequence of digits without commas or decimals and optional sign character
644        /// (ASCII characters "-" and "0" - "9" ). The sign character utilizes one
645        /// byte (i.e. positive int is "99999" while negative int is "-99999"). Note
646        /// that int values may contain leading zeros (e.g. "00023" = "23").
647        /// Examples: 723 in field 21 would be mapped int as |21=723|. -723 in field
648        /// 12 would be mapped int as |12=-723| The following data types are based on
649        /// int.
650        Int,
651        /// int field representing a day during a particular monthy (values 1 to 31).
652        DayOfMonth,
653        /// int field representing the length in bytes. Value must be positive.
654        Length,
655        /// int field representing the number of entries in a repeating group. Value
656        /// must be positive.
657        NumInGroup,
658        /// int field representing a message sequence number. Value must be positive.
659        SeqNum,
660        /// `int` field representing a field's tag number when using FIX "Tag=Value"
661        /// syntax. Value must be positive and may not contain leading zeros.
662        TagNum,
663        /// Alpha-numeric free format strings, can include any character or
664        /// punctuation except the delimiter. All String fields are case sensitive
665        /// (i.e. morstatt != Morstatt).
666        String,
667        /// string field containing raw data with no format or content restrictions.
668        /// Data fields are always immediately preceded by a length field. The length
669        /// field should specify the number of bytes of the value of the data field
670        /// (up to but not including the terminating SOH). Caution: the value of one
671        /// of these fields may contain the delimiter (SOH) character. Note that the
672        /// value specified for this field should be followed by the delimiter (SOH)
673        /// character as all fields are terminated with an "SOH".
674        Data,
675        /// string field representing month of a year. An optional day of the month
676        /// can be appended or an optional week code. Valid formats: YYYYMM YYYYMMDD
677        /// YYYYMMWW Valid values: YYYY = 0000-9999; MM = 01-12; DD = 01-31; WW = w1,
678        /// w2, w3, w4, w5.
679        MonthYear,
680        /// string field containing one or more space delimited single character
681        /// values (e.g. |18=2 A F| ).
682        MultipleCharValue,
683        /// string field representing a currency type using ISO 4217 Currency code (3
684        /// character) values (see Appendix 6-A).
685        Currency,
686        /// string field representing a market or exchange using ISO 10383 Market
687        /// Identifier Code (MIC) values (see"Appendix 6-C).
688        Exchange,
689        /// Identifier for a national language - uses ISO 639-1 standard.
690        Language,
691        /// string field represening a Date of Local Market (as oppose to UTC) in
692        /// YYYYMMDD format. This is the "normal" date field used by the FIX
693        /// Protocol. Valid values: YYYY = 0000-9999, MM = 01-12, DD = 01-31.
694        LocalMktDate,
695        /// string field containing one or more space delimited multiple character
696        /// values (e.g. |277=AV AN A| ).
697        MultipleStringValue,
698        /// string field representing Date represented in UTC (Universal Time
699        /// Coordinated, also known as "GMT") in YYYYMMDD format. This
700        /// special-purpose field is paired with UTCTimeOnly to form a proper
701        /// UTCTimestamp for bandwidth-sensitive messages. Valid values: YYYY =
702        /// 0000-9999, MM = 01-12, DD = 01-31.
703        UtcDateOnly,
704        /// string field representing Time-only represented in UTC (Universal Time
705        /// Coordinated, also known as "GMT") in either HH:MM:SS (whole seconds) or
706        /// HH:MM:SS.sss (milliseconds) format, colons, and period required. This
707        /// special-purpose field is paired with UTCDateOnly to form a proper
708        /// UTCTimestamp for bandwidth-sensitive messages. Valid values: HH = 00-23,
709        /// MM = 00-60 (60 only if UTC leap second), SS = 00-59. (without
710        /// milliseconds) HH = 00-23, MM = 00-59, SS = 00-60 (60 only if UTC leap
711        /// second), sss=000-999 (indicating milliseconds).
712        UtcTimeOnly,
713        /// string field representing Time/date combination represented in UTC
714        /// (Universal Time Coordinated, also known as "GMT") in either
715        /// YYYYMMDD-HH:MM:SS (whole seconds) or YYYYMMDD-HH:MM:SS.sss (milliseconds)
716        /// format, colons, dash, and period required. Valid values: * YYYY =
717        /// 0000-9999, MM = 01-12, DD = 01-31, HH = 00-23, MM = 00-59, SS = 00-60 (60
718        /// only if UTC leap second) (without milliseconds). * YYYY = 0000-9999, MM =
719        /// 01-12, DD = 01-31, HH = 00-23, MM = 00-59, SS = 00-60 (60 only if UTC
720        /// leap second), sss=000-999 (indicating milliseconds). Leap Seconds: Note
721        /// that UTC includes corrections for leap seconds, which are inserted to
722        /// account for slowing of the rotation of the earth. Leap second insertion
723        /// is declared by the International Earth Rotation Service (IERS) and has,
724        /// since 1972, only occurred on the night of Dec. 31 or Jun 30. The IERS
725        /// considers March 31 and September 30 as secondary dates for leap second
726        /// insertion, but has never utilized these dates. During a leap second
727        /// insertion, a UTCTimestamp field may read "19981231-23:59:59",
728        /// "19981231-23:59:60", "19990101-00:00:00". (see
729        /// <http://tycho.usno.navy.mil/leapsec.html>)
730        UtcTimestamp,
731        /// Contains an XML document raw data with no format or content restrictions.
732        /// XMLData fields are always immediately preceded by a length field. The
733        /// length field should specify the number of bytes of the value of the data
734        /// field (up to but not including the terminating SOH).
735        XmlData,
736        /// string field representing a country using ISO 3166 Country code (2
737        /// character) values (see Appendix 6-B).
738        Country,
739    }
740
741    impl FixDatatype {
742        /// Compares `name` to the set of strings commonly used by QuickFIX's custom
743        /// specification format and returns its associated
744        /// [`Datatype`](super::Datatype) if a match
745        /// was found. The query is case-insensitive.
746        ///
747        /// # Examples
748        ///
749        /// ```
750        /// use hotfix_dictionary::FixDatatype;
751        ///
752        /// assert_eq!(FixDatatype::from_quickfix_name("AMT"), Some(FixDatatype::Amt));
753        /// assert_eq!(FixDatatype::from_quickfix_name("Amt"), Some(FixDatatype::Amt));
754        /// assert_eq!(FixDatatype::from_quickfix_name("MONTHYEAR"), Some(FixDatatype::MonthYear));
755        /// assert_eq!(FixDatatype::from_quickfix_name(""), None);
756        /// ```
757        pub fn from_quickfix_name(name: &str) -> Option<Self> {
758            // https://github.com/quickfix/quickfix/blob/b6760f55ac6a46306b4e081bb13b65e6220ab02d/src/C%2B%2B/DataDictionary.cpp#L646-L680
759            Some(match name.to_ascii_uppercase().as_str() {
760                "AMT" => FixDatatype::Amt,
761                "BOOLEAN" => FixDatatype::Boolean,
762                "CHAR" => FixDatatype::Char,
763                "COUNTRY" => FixDatatype::Country,
764                "CURRENCY" => FixDatatype::Currency,
765                "DATA" => FixDatatype::Data,
766                "DATE" => FixDatatype::UtcDateOnly, // FIXME?
767                "DAYOFMONTH" => FixDatatype::DayOfMonth,
768                "EXCHANGE" => FixDatatype::Exchange,
769                "FLOAT" => FixDatatype::Float,
770                "INT" => FixDatatype::Int,
771                "LANGUAGE" => FixDatatype::Language,
772                "LENGTH" => FixDatatype::Length,
773                "LOCALMKTDATE" => FixDatatype::LocalMktDate,
774                "MONTHYEAR" => FixDatatype::MonthYear,
775                "MULTIPLECHARVALUE" | "MULTIPLEVALUESTRING" => FixDatatype::MultipleCharValue,
776                "MULTIPLESTRINGVALUE" => FixDatatype::MultipleStringValue,
777                "NUMINGROUP" => FixDatatype::NumInGroup,
778                "PERCENTAGE" => FixDatatype::Percentage,
779                "PRICE" => FixDatatype::Price,
780                "PRICEOFFSET" => FixDatatype::PriceOffset,
781                "QTY" => FixDatatype::Qty,
782                "STRING" => FixDatatype::String,
783                "TZTIMEONLY" => FixDatatype::UtcTimeOnly, // FIXME
784                "TZTIMESTAMP" => FixDatatype::UtcTimestamp, // FIXME
785                "UTCDATE" => FixDatatype::UtcDateOnly,
786                "UTCDATEONLY" => FixDatatype::UtcDateOnly,
787                "UTCTIMEONLY" => FixDatatype::UtcTimeOnly,
788                "UTCTIMESTAMP" => FixDatatype::UtcTimestamp,
789                "SEQNUM" => FixDatatype::SeqNum,
790                "TIME" => FixDatatype::UtcTimestamp,
791                "XMLDATA" => FixDatatype::XmlData,
792                _ => {
793                    return None;
794                }
795            })
796        }
797
798        /// Returns the name adopted by QuickFIX for `self`.
799        pub fn to_quickfix_name(&self) -> &str {
800            match self {
801                FixDatatype::Int => "INT",
802                FixDatatype::Length => "LENGTH",
803                FixDatatype::Char => "CHAR",
804                FixDatatype::Boolean => "BOOLEAN",
805                FixDatatype::Float => "FLOAT",
806                FixDatatype::Amt => "AMT",
807                FixDatatype::Price => "PRICE",
808                FixDatatype::PriceOffset => "PRICEOFFSET",
809                FixDatatype::Qty => "QTY",
810                FixDatatype::Percentage => "PERCENTAGE",
811                FixDatatype::DayOfMonth => "DAYOFMONTH",
812                FixDatatype::NumInGroup => "NUMINGROUP",
813                FixDatatype::Language => "LANGUAGE",
814                FixDatatype::SeqNum => "SEQNUM",
815                FixDatatype::TagNum => "TAGNUM",
816                FixDatatype::String => "STRING",
817                FixDatatype::Data => "DATA",
818                FixDatatype::MonthYear => "MONTHYEAR",
819                FixDatatype::Currency => "CURRENCY",
820                FixDatatype::Exchange => "EXCHANGE",
821                FixDatatype::LocalMktDate => "LOCALMKTDATE",
822                FixDatatype::MultipleStringValue => "MULTIPLESTRINGVALUE",
823                FixDatatype::UtcTimeOnly => "UTCTIMEONLY",
824                FixDatatype::UtcTimestamp => "UTCTIMESTAMP",
825                FixDatatype::UtcDateOnly => "UTCDATEONLY",
826                FixDatatype::Country => "COUNTRY",
827                FixDatatype::MultipleCharValue => "MULTIPLECHARVALUE",
828                FixDatatype::XmlData => "XMLDATA",
829            }
830        }
831
832        /// Returns the name of `self`, character by character identical to the name
833        /// that appears in the official guidelines. **Generally** primitive datatypes
834        /// will use `snake_case` and non-primitive ones will have `PascalCase`, but
835        /// that's not true for every [`Datatype`](super::Datatype).
836        ///
837        /// # Examples
838        ///
839        /// ```
840        /// use hotfix_dictionary::FixDatatype;
841        ///
842        /// assert_eq!(FixDatatype::Qty.name(), "Qty");
843        /// assert_eq!(FixDatatype::Float.name(), "float");
844        /// assert_eq!(FixDatatype::String.name(), "String");
845        /// ```
846        pub fn name(&self) -> &'static str {
847            // 1. Most primitive data types have `snake_case` names.
848            // 2. Most derivative data types have `PascalCase` names.
849            // 3. `data` and `String` ruin the party and mess it up.
850            //    Why, you ask? Oh, you sweet summer child. You'll learn soon enough
851            //    that nothing makes sense in FIX land.
852            match self {
853                FixDatatype::Int => "int",
854                FixDatatype::Length => "Length",
855                FixDatatype::Char => "char",
856                FixDatatype::Boolean => "Boolean",
857                FixDatatype::Float => "float",
858                FixDatatype::Amt => "Amt",
859                FixDatatype::Price => "Price",
860                FixDatatype::PriceOffset => "PriceOffset",
861                FixDatatype::Qty => "Qty",
862                FixDatatype::Percentage => "Percentage",
863                FixDatatype::DayOfMonth => "DayOfMonth",
864                FixDatatype::NumInGroup => "NumInGroup",
865                FixDatatype::Language => "Language",
866                FixDatatype::SeqNum => "SeqNum",
867                FixDatatype::TagNum => "TagNum",
868                FixDatatype::String => "String",
869                FixDatatype::Data => "data",
870                FixDatatype::MonthYear => "MonthYear",
871                FixDatatype::Currency => "Currency",
872                FixDatatype::Exchange => "Exchange",
873                FixDatatype::LocalMktDate => "LocalMktDate",
874                FixDatatype::MultipleStringValue => "MultipleStringValue",
875                FixDatatype::UtcTimeOnly => "UTCTimeOnly",
876                FixDatatype::UtcTimestamp => "UTCTimestamp",
877                FixDatatype::UtcDateOnly => "UTCDateOnly",
878                FixDatatype::Country => "Country",
879                FixDatatype::MultipleCharValue => "MultipleCharValue",
880                FixDatatype::XmlData => "XMLData",
881            }
882        }
883
884        /// Returns `true` if and only if `self` is a "base type", i.e. a primitive;
885        /// returns `false` otherwise.
886        ///
887        /// # Examples
888        ///
889        /// ```
890        /// use hotfix_dictionary::FixDatatype;
891        ///
892        /// assert_eq!(FixDatatype::Float.is_base_type(), true);
893        /// assert_eq!(FixDatatype::Price.is_base_type(), false);
894        /// ```
895        pub fn is_base_type(&self) -> bool {
896            matches!(self, Self::Char | Self::Float | Self::Int | Self::String)
897        }
898
899        /// Returns the primitive [`Datatype`](super::Datatype) from which `self` is derived. If
900        /// `self` is primitive already, returns `self` unchanged.
901        ///
902        /// # Examples
903        ///
904        /// ```
905        /// use hotfix_dictionary::FixDatatype;
906        ///
907        /// assert_eq!(FixDatatype::Float.base_type(), FixDatatype::Float);
908        /// assert_eq!(FixDatatype::Price.base_type(), FixDatatype::Float);
909        /// ```
910        pub fn base_type(&self) -> Self {
911            let dt = match self {
912                Self::Char | Self::Boolean => Self::Char,
913                Self::Float
914                | Self::Amt
915                | Self::Price
916                | Self::PriceOffset
917                | Self::Qty
918                | Self::Percentage => Self::Float,
919                Self::Int
920                | Self::DayOfMonth
921                | Self::Length
922                | Self::NumInGroup
923                | Self::SeqNum
924                | Self::TagNum => Self::Int,
925                _ => Self::String,
926            };
927            debug_assert!(dt.is_base_type());
928            dt
929        }
930
931        /// Returns an [`Iterator`] over all variants of
932        /// [`Datatype`](super::Datatype).
933        pub fn iter_all() -> impl Iterator<Item = Self> {
934            <Self as IntoEnumIterator>::iter()
935        }
936    }
937
938    #[cfg(test)]
939    mod test {
940        use super::*;
941        use std::collections::HashSet;
942
943        #[test]
944        fn iter_all_unique() {
945            let as_vec = FixDatatype::iter_all().collect::<Vec<FixDatatype>>();
946            let as_set = FixDatatype::iter_all().collect::<HashSet<FixDatatype>>();
947            assert_eq!(as_vec.len(), as_set.len());
948        }
949
950        #[test]
951        fn more_than_20_datatypes() {
952            // According to the official documentation, FIX has "about 20 data
953            // types". Including recent revisions, we should well exceed that
954            // number.
955            assert!(FixDatatype::iter_all().count() > 20);
956        }
957
958        #[test]
959        fn names_are_unique() {
960            let as_vec = FixDatatype::iter_all()
961                .map(|dt| dt.name())
962                .collect::<Vec<&str>>();
963            let as_set = FixDatatype::iter_all()
964                .map(|dt| dt.name())
965                .collect::<HashSet<&str>>();
966            assert_eq!(as_vec.len(), as_set.len());
967        }
968
969        #[test]
970        fn base_type_is_itself() {
971            for dt in FixDatatype::iter_all() {
972                if dt.is_base_type() {
973                    assert_eq!(dt.base_type(), dt);
974                } else {
975                    assert_ne!(dt.base_type(), dt);
976                }
977            }
978        }
979
980        #[test]
981        fn base_type_is_actually_base_type() {
982            for dt in FixDatatype::iter_all() {
983                assert!(dt.base_type().is_base_type());
984            }
985        }
986    }
987}
988
989/// A field is identified by a unique tag number and a name. Each field in a
990/// message is associated with a value.
991#[derive(Clone, Debug)]
992struct FieldData {
993    /// A human readable string representing the name of the field.
994    name: SmartString,
995    /// **Primary key.** A positive integer representing the unique
996    /// identifier for this field type.
997    tag: u32,
998    /// The datatype of the field.
999    data_type_name: SmartString,
1000    /// The associated data field. If given, this field represents the length of
1001    /// the referenced data field
1002    associated_data_tag: Option<usize>,
1003    value_restrictions: Option<Vec<FieldEnumData>>,
1004    /// Indicates whether the field is required in an XML message.
1005    required: bool,
1006    description: Option<String>,
1007}
1008
1009#[derive(Clone, Debug)]
1010struct FieldEnumData {
1011    value: String,
1012    description: String,
1013}
1014
1015/// A limitation imposed on the value of a specific FIX [`Field`].  Also known as
1016/// "code set".
1017#[derive(Debug)]
1018#[allow(dead_code)]
1019pub struct FieldEnum<'a>(&'a Dictionary, &'a FieldEnumData);
1020
1021impl<'a> FieldEnum<'a> {
1022    /// Returns the string representation of this field variant.
1023    pub fn value(&self) -> &str {
1024        &self.1.value[..]
1025    }
1026
1027    /// Returns the documentation description for `self`.
1028    pub fn description(&self) -> &str {
1029        &self.1.description[..]
1030    }
1031}
1032
1033/// A field is the most granular message structure abstraction. It carries a
1034/// specific business meaning as described by the FIX specifications. The data
1035/// domain of a [`Field`] is either a [`Datatype`] or a "code set", i.e.
1036/// enumeration.
1037#[derive(Debug, Copy, Clone)]
1038pub struct Field<'a>(&'a Dictionary, &'a FieldData);
1039
1040impl<'a> Field<'a> {
1041    pub fn doc_url_onixs(&self, version: &str) -> String {
1042        let v = match version {
1043            "FIX.4.0" => "4.0",
1044            "FIX.4.1" => "4.1",
1045            "FIX.4.2" => "4.2",
1046            "FIX.4.3" => "4.3",
1047            "FIX.4.4" => "4.4",
1048            "FIX.5.0" => "5.0",
1049            "FIX.5.0SP1" => "5.0.SP1",
1050            "FIX.5.0SP2" => "5.0.SP2",
1051            "FIXT.1.1" => "FIXT.1.1",
1052            s => s,
1053        };
1054        format!(
1055            "https://www.onixs.biz/fix-dictionary/{}/tagNum_{}.html",
1056            v,
1057            self.1.tag.to_string().as_str()
1058        )
1059    }
1060
1061    pub fn is_num_in_group(&self) -> bool {
1062        fn nth_char_is_uppercase(s: &str, i: usize) -> bool {
1063            s.chars().nth(i).map(|c| c.is_ascii_uppercase()) == Some(true)
1064        }
1065
1066        self.fix_datatype().base_type() == FixDatatype::NumInGroup
1067            || self.name().ends_with("Len")
1068            || (self.name().starts_with("No") && nth_char_is_uppercase(self.name(), 2))
1069    }
1070
1071    /// Returns the [`FixDatatype`] of `self`.
1072    pub fn fix_datatype(&self) -> FixDatatype {
1073        self.data_type().basetype()
1074    }
1075
1076    /// Returns the name of `self`. Field names are unique across each FIX
1077    /// [`Dictionary`].
1078    pub fn name(&self) -> &str {
1079        self.1.name.as_str()
1080    }
1081
1082    /// Returns the numeric tag of `self`. Field tags are unique across each FIX
1083    /// [`Dictionary`].
1084    pub fn tag(&self) -> TagU32 {
1085        TagU32::new(self.1.tag).unwrap()
1086    }
1087
1088    /// In case this field allows any value, it returns `None`; otherwise; it
1089    /// returns an [`Iterator`] of all allowed values.
1090    pub fn enums(&self) -> Option<impl Iterator<Item = FieldEnum<'_>>> {
1091        self.1
1092            .value_restrictions
1093            .as_ref()
1094            .map(move |v| v.iter().map(move |f| FieldEnum(self.0, f)))
1095    }
1096
1097    /// Returns the [`Datatype`] of `self`.
1098    pub fn data_type(&self) -> Datatype<'_> {
1099        self.0
1100            .datatype_by_name(self.1.data_type_name.as_str())
1101            .unwrap()
1102    }
1103
1104    pub fn data_tag(&self) -> Option<TagU32> {
1105        self.1
1106            .associated_data_tag
1107            .map(|tag| TagU32::new(tag as u32).unwrap())
1108    }
1109
1110    pub fn required_in_xml_messages(&self) -> bool {
1111        self.1.required
1112    }
1113
1114    pub fn description(&self) -> Option<&str> {
1115        self.1.description.as_deref()
1116    }
1117}
1118
1119impl<'a> IsFieldDefinition for Field<'a> {
1120    fn name(&self) -> &str {
1121        self.1.name.as_str()
1122    }
1123
1124    fn tag(&self) -> TagU32 {
1125        TagU32::new(self.1.tag).expect("Invalid FIX tag (0)")
1126    }
1127
1128    fn location(&self) -> FieldLocation {
1129        FieldLocation::Body // FIXME
1130    }
1131}
1132
1133#[derive(Clone, Debug)]
1134#[allow(dead_code)]
1135enum LayoutItemKindData {
1136    Component {
1137        name: SmartString,
1138    },
1139    Group {
1140        len_field_tag: u32,
1141        items: Vec<LayoutItemData>,
1142    },
1143    Field {
1144        tag: u32,
1145    },
1146}
1147
1148#[derive(Clone, Debug)]
1149struct LayoutItemData {
1150    required: bool,
1151    kind: LayoutItemKindData,
1152}
1153
1154pub trait IsFieldDefinition {
1155    /// Returns the FIX tag associated with `self`.
1156    fn tag(&self) -> TagU32;
1157
1158    /// Returns the official, ASCII, human-readable name associated with `self`.
1159    fn name(&self) -> &str;
1160
1161    /// Returns the field location of `self`.
1162    fn location(&self) -> FieldLocation;
1163}
1164
1165fn layout_item_kind<'a>(item: &'a LayoutItemKindData, dict: &'a Dictionary) -> LayoutItemKind<'a> {
1166    match item {
1167        LayoutItemKindData::Component { name } => {
1168            LayoutItemKind::Component(dict.component_by_name(name).unwrap())
1169        }
1170        LayoutItemKindData::Group {
1171            len_field_tag,
1172            items: items_data,
1173        } => {
1174            let items = items_data
1175                .iter()
1176                .map(|item_data| LayoutItem(dict, item_data))
1177                .collect::<Vec<_>>();
1178            let len_field = dict.field_by_tag(*len_field_tag).unwrap();
1179            LayoutItemKind::Group(len_field, items)
1180        }
1181        LayoutItemKindData::Field { tag } => {
1182            LayoutItemKind::Field(dict.field_by_tag(*tag).unwrap())
1183        }
1184    }
1185}
1186
1187/// An entry in a sequence of FIX field definitions.
1188#[derive(Clone, Debug)]
1189pub struct LayoutItem<'a>(&'a Dictionary, &'a LayoutItemData);
1190
1191/// The kind of element contained in a [`Message`].
1192#[derive(Debug)]
1193pub enum LayoutItemKind<'a> {
1194    /// This component item is another component.
1195    Component(Component<'a>),
1196    /// This component item is a FIX repeating group.
1197    Group(Field<'a>, Vec<LayoutItem<'a>>),
1198    /// This component item is a FIX field.
1199    Field(Field<'a>),
1200}
1201
1202impl<'a> LayoutItem<'a> {
1203    /// Returns `true` if `self` is required in order to have a valid definition
1204    /// of its parent container, `false` otherwise.
1205    pub fn required(&self) -> bool {
1206        self.1.required
1207    }
1208
1209    /// Returns the [`LayoutItemKind`] of `self`.
1210    pub fn kind(&self) -> LayoutItemKind<'_> {
1211        layout_item_kind(&self.1.kind, self.0)
1212    }
1213
1214    /// Returns the human-readable name of `self`.
1215    pub fn tag_text(&self) -> String {
1216        match &self.1.kind {
1217            LayoutItemKindData::Component { name } => {
1218                self.0.component_by_name(name).unwrap().name().to_string()
1219            }
1220            LayoutItemKindData::Group {
1221                len_field_tag,
1222                items: _items,
1223            } => self
1224                .0
1225                .field_by_tag(*len_field_tag)
1226                .unwrap()
1227                .name()
1228                .to_string(),
1229            LayoutItemKindData::Field { tag } => {
1230                self.0.field_by_tag(*tag).unwrap().name().to_string()
1231            }
1232        }
1233    }
1234}
1235
1236type LayoutItems = Vec<LayoutItemData>;
1237
1238#[derive(Clone, Debug)]
1239struct MessageData {
1240    /// The unique integer identifier of this message type.
1241    component_id: u32,
1242    /// **Primary key**. The unique character identifier of this message
1243    /// type; used literally in FIX messages.
1244    msg_type: SmartString,
1245    /// The name of this message type.
1246    name: SmartString,
1247    layout_items: LayoutItems,
1248    /// A boolean used to indicate if the message is to be generated as part
1249    /// of FIXML.
1250    required: bool,
1251    description: String,
1252}
1253
1254/// A [`Message`] is a unit of information sent on the wire between
1255/// counterparties. Every [`Message`] is composed of fields and/or components.
1256#[derive(Debug)]
1257pub struct Message<'a>(&'a Dictionary, &'a MessageData);
1258
1259impl<'a> Message<'a> {
1260    /// Returns the human-readable name of `self`.
1261    pub fn name(&self) -> &str {
1262        self.1.name.as_str()
1263    }
1264
1265    /// Returns the message type of `self`.
1266    pub fn msg_type(&self) -> &str {
1267        self.1.msg_type.as_str()
1268    }
1269
1270    /// Returns the description associated with `self`.
1271    pub fn description(&self) -> &str {
1272        &self.1.description
1273    }
1274
1275    pub fn group_info(&self, num_in_group_tag: TagU32) -> Option<TagU32> {
1276        self.layout().find_map(|layout_item| {
1277            if let LayoutItemKind::Group(field, items) = layout_item.kind() {
1278                if field.tag() == num_in_group_tag {
1279                    if let LayoutItemKind::Field(f) = items[0].kind() {
1280                        Some(f.tag())
1281                    } else {
1282                        None
1283                    }
1284                } else {
1285                    None
1286                }
1287            } else if let LayoutItemKind::Component(_component) = layout_item.kind() {
1288                None
1289            } else {
1290                None
1291            }
1292        })
1293    }
1294
1295    /// Returns the component ID of `self`.
1296    pub fn component_id(&self) -> u32 {
1297        self.1.component_id
1298    }
1299
1300    pub fn layout(&self) -> impl Iterator<Item = LayoutItem<'_>> {
1301        self.1
1302            .layout_items
1303            .iter()
1304            .map(move |data| LayoutItem(self.0, data))
1305    }
1306
1307    pub fn fixml_required(&self) -> bool {
1308        self.1.required
1309    }
1310}
1311
1312/// A [`Section`] is a collection of many [`Component`]-s. It has no practical
1313/// effect on encoding and decoding of FIX data and it's only used for
1314/// documentation and human readability.
1315#[derive(Clone, Debug, PartialEq)]
1316pub struct Section {}
1317
1318#[cfg(test)]
1319mod test {
1320    use super::*;
1321    use std::collections::HashSet;
1322
1323    #[test]
1324    fn fix44_quickfix_is_ok() {
1325        let dict = Dictionary::fix44();
1326        let msg_heartbeat = dict.message_by_name("Heartbeat").unwrap();
1327        assert_eq!(msg_heartbeat.msg_type(), "0");
1328        assert_eq!(msg_heartbeat.name(), "Heartbeat".to_string());
1329        assert!(msg_heartbeat.layout().any(|c| {
1330            if let LayoutItemKind::Field(f) = c.kind() {
1331                f.name() == "TestReqID"
1332            } else {
1333                false
1334            }
1335        }));
1336    }
1337
1338    #[test]
1339    fn all_datatypes_are_used_at_least_once() {
1340        for dict in Dictionary::common_dictionaries().iter() {
1341            let datatypes_count = dict.datatypes().len();
1342            let mut datatypes = HashSet::new();
1343            for field in dict.fields() {
1344                datatypes.insert(field.data_type().name().to_string());
1345            }
1346            assert_eq!(datatypes_count, datatypes.len());
1347        }
1348    }
1349
1350    #[test]
1351    fn at_least_one_datatype() {
1352        for dict in Dictionary::common_dictionaries().iter() {
1353            assert!(!dict.datatypes().is_empty());
1354        }
1355    }
1356
1357    #[test]
1358    fn std_header_and_trailer_always_present() {
1359        for dict in Dictionary::common_dictionaries().iter() {
1360            let std_header = dict.component_by_name("StandardHeader");
1361            let std_trailer = dict.component_by_name("StandardTrailer");
1362            assert!(std_header.is_some() && std_trailer.is_some());
1363        }
1364    }
1365
1366    #[test]
1367    fn fix44_field_28_has_three_variants() {
1368        let dict = Dictionary::fix44();
1369        let field_28 = dict.field_by_tag(28).unwrap();
1370        assert_eq!(field_28.name(), "IOITransType");
1371        assert_eq!(field_28.enums().unwrap().count(), 3);
1372    }
1373
1374    #[test]
1375    fn fix44_field_36_has_no_variants() {
1376        let dict = Dictionary::fix44();
1377        let field_36 = dict.field_by_tag(36).unwrap();
1378        assert_eq!(field_36.name(), "NewSeqNo");
1379        assert!(field_36.enums().is_none());
1380    }
1381
1382    #[test]
1383    fn fix44_field_167_has_eucorp_variant() {
1384        let dict = Dictionary::fix44();
1385        let field_167 = dict.field_by_tag(167).unwrap();
1386        assert_eq!(field_167.name(), "SecurityType");
1387        assert!(field_167.enums().unwrap().any(|e| e.value() == "EUCORP"));
1388    }
1389
1390    const INVALID_QUICKFIX_SPECS: &[&str] = &[
1391        include_str!("test_data/quickfix_specs/empty_file.xml"),
1392        include_str!("test_data/quickfix_specs/missing_components.xml"),
1393        include_str!("test_data/quickfix_specs/missing_fields.xml"),
1394        include_str!("test_data/quickfix_specs/missing_header.xml"),
1395        include_str!("test_data/quickfix_specs/missing_messages.xml"),
1396        include_str!("test_data/quickfix_specs/missing_trailer.xml"),
1397        include_str!("test_data/quickfix_specs/root_has_no_type_attr.xml"),
1398        include_str!("test_data/quickfix_specs/root_has_no_version_attrs.xml"),
1399        include_str!("test_data/quickfix_specs/root_is_not_fix.xml"),
1400    ];
1401
1402    #[test]
1403    fn invalid_quickfix_specs() {
1404        for spec in INVALID_QUICKFIX_SPECS.iter() {
1405            let dict = Dictionary::from_quickfix_spec(spec);
1406            assert!(dict.is_err(), "{}", spec);
1407        }
1408    }
1409}