hotfix-dictionary 0.1.5

FIX dictionary generation for HotFIX.
Documentation
//! Access to FIX Dictionary reference and message specifications.

mod builder;
mod component;
mod datatype;
mod dictionary;
mod error;
mod field;
mod layout;
mod message_definition;
mod quickfix;
mod string;

use component::{Component, ComponentData};
use datatype::DatatypeData;
pub use datatype::{Datatype, FixDatatype};
pub use dictionary::Dictionary;
pub use error::ParseError;
pub use field::{Field, FieldEnum, FieldLocation, IsFieldDefinition};
use field::{FieldData, FieldEnumData};
use fnv::FnvHashMap;
pub use layout::{LayoutItem, LayoutItemKind, display_layout_item};
use layout::{LayoutItemData, LayoutItemKindData, LayoutItems};
use std::sync::Arc;

/// A mapping from FIX version strings to [`Dictionary`] values.
pub type Dictionaries = FnvHashMap<String, Arc<Dictionary>>;

/// Type alias for FIX tags: 32-bit unsigned integers, strictly positive.
pub type TagU32 = std::num::NonZeroU32;

#[cfg(test)]
mod test {
    use super::*;
    #[cfg(feature = "fix44")]
    use crate::layout::LayoutItemKind;
    use std::collections::HashSet;

    #[test]
    #[cfg(feature = "fix44")]
    fn fix44_quickfix_is_ok() {
        let dict = Dictionary::fix44();
        let msg_heartbeat = dict.message_by_name("Heartbeat").unwrap();
        assert_eq!(msg_heartbeat.msg_type(), "0");
        assert_eq!(msg_heartbeat.name(), "Heartbeat".to_string());
        assert!(msg_heartbeat.layout().any(|c| {
            if let LayoutItemKind::Field(f) = c.kind() {
                f.name() == "TestReqID"
            } else {
                false
            }
        }));
    }

    #[test]
    fn all_datatypes_are_used_at_least_once() {
        for dict in Dictionary::common_dictionaries().iter() {
            let datatypes_count = dict.datatypes().len();
            let mut datatypes = HashSet::new();
            for field in dict.fields() {
                datatypes.insert(field.data_type().name().to_string());
            }
            assert_eq!(datatypes_count, datatypes.len());
        }
    }

    #[test]
    fn at_least_one_datatype() {
        for dict in Dictionary::common_dictionaries().iter() {
            assert!(!dict.datatypes().is_empty());
        }
    }

    #[test]
    fn std_header_and_trailer_always_present() {
        for dict in Dictionary::common_dictionaries().iter() {
            let std_header = dict.component_by_name("StandardHeader");
            let std_trailer = dict.component_by_name("StandardTrailer");
            assert!(std_header.is_some() && std_trailer.is_some());
        }
    }

    #[test]
    #[cfg(feature = "fix44")]
    fn fix44_field_28_has_three_variants() {
        let dict = Dictionary::fix44();
        let field_28 = dict.field_by_tag(28).unwrap();
        assert_eq!(field_28.name(), "IOITransType");
        assert_eq!(field_28.enums().unwrap().count(), 3);
    }

    #[test]
    #[cfg(feature = "fix44")]
    fn fix44_field_36_has_no_variants() {
        let dict = Dictionary::fix44();
        let field_36 = dict.field_by_tag(36).unwrap();
        assert_eq!(field_36.name(), "NewSeqNo");
        assert!(field_36.enums().is_none());
    }

    #[test]
    #[cfg(feature = "fix44")]
    fn fix44_field_167_has_eucorp_variant() {
        let dict = Dictionary::fix44();
        let field_167 = dict.field_by_tag(167).unwrap();
        assert_eq!(field_167.name(), "SecurityType");
        assert!(field_167.enums().unwrap().any(|e| e.value() == "EUCORP"));
    }

    const INVALID_QUICKFIX_SPECS: &[&str] = &[
        include_str!("test_data/quickfix_specs/empty_file.xml"),
        include_str!("test_data/quickfix_specs/missing_components.xml"),
        include_str!("test_data/quickfix_specs/missing_fields.xml"),
        include_str!("test_data/quickfix_specs/missing_header.xml"),
        include_str!("test_data/quickfix_specs/missing_messages.xml"),
        include_str!("test_data/quickfix_specs/missing_trailer.xml"),
        include_str!("test_data/quickfix_specs/root_has_no_type_attr.xml"),
        include_str!("test_data/quickfix_specs/root_has_no_version_attrs.xml"),
        include_str!("test_data/quickfix_specs/root_is_not_fix.xml"),
    ];

    #[test]
    fn invalid_quickfix_specs() {
        for spec in INVALID_QUICKFIX_SPECS.iter() {
            let dict = Dictionary::from_quickfix_spec(spec);
            assert!(dict.is_err(), "{}", spec);
        }
    }
}