#![cfg_attr(doc_cfg, feature(doc_cfg))]
#![allow(unexpected_cfgs)]
pub mod builder;
mod fix_datatype;
mod quickfix;
use builder::{
AbbreviationData, CategoryData, ComponentData, DatatypeData, FieldData, FieldEnumData,
LayoutItemData, LayoutItemKindData, MessageData,
};
pub use fix_datatype::FixDatatype;
use quickfix::{ParseDictionaryError, QuickFixReader};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use smartstring::alias::String as SmartString;
use std::sync::Arc;
pub type TagU32 = std::num::NonZeroU32;
pub trait DataFieldLookup<F> {
fn field_is_data(&self, field: F) -> bool;
}
pub trait NumInGroupLookup<F> {
fn field_is_num_in_group(&self, field: F) -> bool;
}
impl DataFieldLookup<u32> for Dictionary {
fn field_is_data(&self, tag: u32) -> bool {
if let Some(field) = self.field_by_tag(tag) {
field.data_type().basetype() == FixDatatype::Data
} else {
false
}
}
}
impl NumInGroupLookup<u32> for Dictionary {
fn field_is_num_in_group(&self, tag: u32) -> bool {
if let Some(field) = self.field_by_tag(tag) {
field.data_type().basetype() == FixDatatype::NumInGroup
} else {
false
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FieldLocation {
Header,
Body,
Trailer,
}
pub type Dictionaries = FxHashMap<SmartString, Arc<Dictionary>>;
#[derive(Debug, Clone)]
pub struct Dictionary {
version: SmartString,
abbreviation_definitions: FxHashMap<SmartString, AbbreviationData>,
data_types_by_name: FxHashMap<SmartString, DatatypeData>,
fields_by_tags: FxHashMap<u32, FieldData>,
field_tags_by_name: FxHashMap<SmartString, u32>,
components_by_name: FxHashMap<SmartString, ComponentData>,
messages_by_msgtype: FxHashMap<SmartString, MessageData>,
message_msgtypes_by_name: FxHashMap<SmartString, SmartString>,
categories_by_name: FxHashMap<SmartString, CategoryData>,
}
impl Dictionary {
fn new<S: ToString>(version: S) -> Self {
Dictionary {
version: version.to_string().into(),
abbreviation_definitions: FxHashMap::default(),
data_types_by_name: FxHashMap::default(),
fields_by_tags: FxHashMap::default(),
field_tags_by_name: FxHashMap::default(),
components_by_name: FxHashMap::default(),
messages_by_msgtype: FxHashMap::default(),
message_msgtypes_by_name: FxHashMap::default(),
categories_by_name: FxHashMap::default(),
}
}
pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseDictionaryError> {
let xml_document =
roxmltree::Document::parse(input).map_err(|_| ParseDictionaryError::InvalidFormat)?;
QuickFixReader::from_xml(&xml_document)
}
pub fn version(&self) -> &str {
self.version.as_str()
}
#[cfg(feature = "fix40")]
pub fn fix40() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-4.0.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fix41")]
pub fn fix41() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-4.1.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fix42")]
pub fn fix42() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-4.2.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fix43")]
pub fn fix43() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-4.3.xml");
Dictionary::from_quickfix_spec(spec)
}
pub fn fix44() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-4.4.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fix50")]
pub fn fix50() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-5.0.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fix50sp1")]
pub fn fix50sp1() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-5.0-SP1.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fix50sp2")]
pub fn fix50sp2() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIX-5.0-SP2.xml");
Dictionary::from_quickfix_spec(spec)
}
#[cfg(feature = "fixt11")]
pub fn fixt11() -> Result<Self, ParseDictionaryError> {
let spec = include_str!("resources/quickfix/FIXT-1.1.xml");
Dictionary::from_quickfix_spec(spec)
}
pub fn common_dictionaries() -> SmallVec<[Dictionary; 10]> {
#[allow(unused_mut)] let mut dictionaries = SmallVec::new();
#[cfg(feature = "fix40")]
dictionaries.push(Self::fix40().expect("Failed to parse FIX 4.0 dictionary"));
#[cfg(feature = "fix41")]
dictionaries.push(Self::fix41().expect("Failed to parse FIX 4.1 dictionary"));
#[cfg(feature = "fix42")]
dictionaries.push(Self::fix42().expect("Failed to parse FIX 4.2 dictionary"));
#[cfg(feature = "fix43")]
dictionaries.push(Self::fix43().expect("Failed to parse FIX 4.3 dictionary"));
#[cfg(feature = "fix44")]
dictionaries.push(Self::fix44().expect("Failed to parse FIX 4.4 dictionary"));
#[cfg(feature = "fix50")]
dictionaries.push(Self::fix50().expect("Failed to parse FIX 5.0 dictionary"));
#[cfg(feature = "fix50sp1")]
dictionaries.push(Self::fix50sp1().expect("Failed to parse FIX 5.0 SP1 dictionary"));
#[cfg(feature = "fix50sp2")]
dictionaries.push(Self::fix50sp2().expect("Failed to parse FIX 5.0 SP2 dictionary"));
#[cfg(feature = "fixt11")]
dictionaries.push(Self::fixt11().expect("Failed to parse FIXT 1.1 dictionary"));
dictionaries
}
pub fn abbreviation_for(&self, term: &str) -> Option<Abbreviation> {
self.abbreviation_definitions.get(term).map(Abbreviation)
}
pub fn message_by_name(&self, name: &str) -> Option<Message> {
let msg_type = self.message_msgtypes_by_name.get(name)?;
self.message_by_msgtype(msg_type)
}
pub fn message_by_msgtype(&self, msgtype: &str) -> Option<Message> {
self.messages_by_msgtype
.get(msgtype)
.map(|data| Message(self, data))
}
pub fn component_by_name(&self, name: &str) -> Option<Component> {
self.components_by_name
.get(name)
.map(|data| Component(data, self))
}
pub fn datatype_by_name(&self, name: &str) -> Option<Datatype> {
self.data_types_by_name.get(name).map(Datatype)
}
pub fn field_by_tag(&self, tag: u32) -> Option<Field> {
self.fields_by_tags.get(&tag).map(|data| Field(self, data))
}
pub fn field_by_name(&self, name: &str) -> Option<Field> {
let tag = self.field_tags_by_name.get(name)?;
self.field_by_tag(*tag)
}
fn category_by_name(&self, name: &str) -> Option<Category> {
self.categories_by_name.get(name).map(Category)
}
pub fn datatypes(&self) -> SmallVec<[Datatype; 24]> {
self.data_types_by_name.values().map(Datatype).collect()
}
pub fn messages(&self) -> Vec<Message> {
self.messages_by_msgtype
.values()
.map(|data| Message(self, data))
.collect()
}
pub fn categories(&self) -> SmallVec<[Category; 8]> {
self.categories_by_name.values().map(Category).collect()
}
pub fn fields(&self) -> Vec<Field> {
self.fields_by_tags
.values()
.map(|data| Field(self, data))
.collect()
}
pub fn components(&self) -> Vec<Component> {
self.components_by_name
.values()
.map(|data| Component(data, self))
.collect()
}
}
#[derive(Debug)]
pub struct Abbreviation<'a>(&'a AbbreviationData);
impl<'a> Abbreviation<'a> {
pub fn term(&self) -> &str {
self.0.abbreviation.as_str()
}
}
#[derive(Clone, Debug)]
pub struct Category<'a>(&'a CategoryData);
impl<'a> Category<'a> {
pub fn name(&self) -> &str {
self.0.name.as_str()
}
}
#[derive(Clone, Debug)]
pub struct Component<'a>(&'a ComponentData, &'a Dictionary);
impl<'a> Component<'a> {
pub fn id(&self) -> u32 {
self.0.id as u32
}
pub fn name(&self) -> &str {
self.0.name.as_str()
}
pub fn is_group(&self) -> bool {
match self.0.component_type {
FixmlComponentAttributes::Block { is_repeating, .. } => is_repeating,
_ => false,
}
}
pub fn category(&self) -> Category {
self.1
.category_by_name(self.0.category_name.as_str())
.unwrap()
}
pub fn items(&self) -> impl Iterator<Item = LayoutItem> {
self.0
.layout_items
.iter()
.map(move |data| LayoutItem(self.1, data))
}
pub fn contains_field(&self, field: &Field) -> bool {
self.items().any(|layout_item| {
if let LayoutItemKind::Field(f) = layout_item.kind() {
f.tag() == field.tag()
} else {
false
}
})
}
}
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)]
pub enum FixmlComponentAttributes {
Xml,
Block {
is_repeating: bool,
is_implicit: bool,
is_optimized: bool,
},
Message,
}
#[derive(Debug)]
pub struct Datatype<'a>(&'a DatatypeData);
impl<'a> Datatype<'a> {
pub fn name(&self) -> &str {
self.0.datatype.name()
}
pub fn basetype(&self) -> FixDatatype {
self.0.datatype
}
}
#[derive(Debug)]
pub struct FieldEnum<'a>(&'a FieldEnumData);
impl<'a> FieldEnum<'a> {
pub fn value(&self) -> &str {
&self.0.value[..]
}
pub fn description(&self) -> &str {
&self.0.description[..]
}
}
#[derive(Debug, Copy, Clone)]
pub struct Field<'a>(&'a Dictionary, &'a FieldData);
impl<'a> Field<'a> {
pub fn doc_url_onixs(&self, version: &str) -> SmartString {
let v = match version {
"FIX.4.0" => "4.0",
"FIX.4.1" => "4.1",
"FIX.4.2" => "4.2",
"FIX.4.3" => "4.3",
"FIX.4.4" => "4.4",
"FIX.5.0" => "5.0",
"FIX.5.0SP1" => "5.0.SP1",
"FIX.5.0SP2" => "5.0.SP2",
"FIXT.1.1" => "FIXT.1.1",
s => s,
};
format!(
"https://www.onixs.biz/fix-dictionary/{}/tagNum_{}.html",
v,
self.1.tag.to_string().as_str()
)
.into()
}
pub fn is_num_in_group(&self) -> bool {
fn nth_char_is_uppercase(s: &str, i: usize) -> bool {
s.chars().nth(i).map(|c| c.is_ascii_uppercase()) == Some(true)
}
self.fix_datatype().base_type() == FixDatatype::NumInGroup
|| self.name().ends_with("Len")
|| (self.name().starts_with("No") && nth_char_is_uppercase(self.name(), 2))
}
pub fn fix_datatype(&self) -> FixDatatype {
self.data_type().basetype()
}
pub fn name(&self) -> &str {
self.1.name.as_str()
}
pub fn tag(&self) -> TagU32 {
TagU32::new(self.1.tag).unwrap()
}
pub fn enums(&self) -> Option<impl Iterator<Item = FieldEnum>> {
self.1
.value_restrictions
.as_ref()
.map(move |v| v.iter().map(FieldEnum))
}
pub fn data_type(&self) -> Datatype {
self.0
.datatype_by_name(self.1.data_type_name.as_str())
.unwrap()
}
pub fn data_tag(&self) -> Option<TagU32> {
self.1
.associated_data_tag
.map(|tag| TagU32::new(tag as u32).unwrap())
}
pub fn required_in_xml_messages(&self) -> bool {
self.1.required
}
pub fn description(&self) -> Option<&str> {
self.1.description.as_deref()
}
}
impl<'a> IsFieldDefinition for Field<'a> {
fn name(&self) -> &str {
self.1.name.as_str()
}
fn tag(&self) -> TagU32 {
TagU32::new(self.1.tag).expect("Invalid FIX tag (0)")
}
fn location(&self) -> FieldLocation {
FieldLocation::Body }
}
pub trait IsFieldDefinition {
fn tag(&self) -> TagU32;
fn name(&self) -> &str;
fn location(&self) -> FieldLocation;
}
fn layout_item_kind<'a>(item: &'a LayoutItemKindData, dict: &'a Dictionary) -> LayoutItemKind<'a> {
match item {
LayoutItemKindData::Component { name } => {
LayoutItemKind::Component(dict.component_by_name(name).unwrap())
}
LayoutItemKindData::Group {
len_field_tag,
items: items_data,
} => {
let items = items_data
.iter()
.map(|item_data| LayoutItem(dict, item_data))
.collect::<SmallVec<[_; 8]>>();
let len_field = dict.field_by_tag(*len_field_tag).unwrap();
LayoutItemKind::Group(len_field, items)
}
LayoutItemKindData::Field { tag } => {
LayoutItemKind::Field(dict.field_by_tag(*tag).unwrap())
}
}
}
#[derive(Clone, Debug)]
pub struct LayoutItem<'a>(&'a Dictionary, &'a LayoutItemData);
#[derive(Debug)]
pub enum LayoutItemKind<'a> {
Component(Component<'a>),
Group(Field<'a>, SmallVec<[LayoutItem<'a>; 8]>),
Field(Field<'a>),
}
impl<'a> LayoutItem<'a> {
pub fn required(&self) -> bool {
self.1.required
}
pub fn kind(&self) -> LayoutItemKind {
layout_item_kind(&self.1.kind, self.0)
}
pub fn tag_text(&self) -> SmartString {
match &self.1.kind {
LayoutItemKindData::Component { name } => {
self.0.component_by_name(name).unwrap().name().into()
}
LayoutItemKindData::Group {
len_field_tag,
items: _items,
} => self.0.field_by_tag(*len_field_tag).unwrap().name().into(),
LayoutItemKindData::Field { tag } => self.0.field_by_tag(*tag).unwrap().name().into(),
}
}
}
#[derive(Debug)]
pub struct Message<'a>(&'a Dictionary, &'a MessageData);
impl<'a> Message<'a> {
pub fn name(&self) -> &str {
self.1.name.as_str()
}
pub fn msg_type(&self) -> &str {
self.1.msg_type.as_str()
}
pub fn description(&self) -> &str {
&self.1.description
}
pub fn group_info(&self, num_in_group_tag: TagU32) -> Option<TagU32> {
self.layout().find_map(|layout_item| {
if let LayoutItemKind::Group(field, items) = layout_item.kind() {
if field.tag() == num_in_group_tag {
if let LayoutItemKind::Field(f) = items[0].kind() {
Some(f.tag())
} else {
None
}
} else {
None
}
} else if let LayoutItemKind::Component(_component) = layout_item.kind() {
None
} else {
None
}
})
}
pub fn component_id(&self) -> u32 {
self.1.component_id
}
pub fn layout(&self) -> impl Iterator<Item = LayoutItem> {
self.1
.layout_items
.iter()
.map(move |data| LayoutItem(self.0, data))
}
pub fn fixml_required(&self) -> bool {
self.1.required
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Section {}
#[cfg(test)]
mod test {
use super::*;
use rustc_hash::FxHashSet;
#[test]
fn fix44_quickfix_is_ok() {
let dict = Dictionary::fix44().unwrap();
let msg_heartbeat = dict.message_by_name("Heartbeat").unwrap();
assert_eq!(msg_heartbeat.msg_type(), "0");
assert_eq!(msg_heartbeat.name(), "Heartbeat");
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: FxHashSet<SmartString> = FxHashSet::default();
for field in dict.fields() {
datatypes.insert(field.data_type().name().into());
}
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]
fn fix44_field_28_has_three_variants() {
let dict = Dictionary::fix44().unwrap();
let field_28 = dict.field_by_tag(28).unwrap();
assert_eq!(field_28.name(), "IOITransType");
assert_eq!(field_28.enums().unwrap().count(), 3);
}
#[test]
fn fix44_field_36_has_no_variants() {
let dict = Dictionary::fix44().unwrap();
let field_36 = dict.field_by_tag(36).unwrap();
assert_eq!(field_36.name(), "NewSeqNo");
assert!(field_36.enums().is_none());
}
#[test]
fn fix44_field_167_has_eucorp_variant() {
let dict = Dictionary::fix44().unwrap();
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);
}
}
}