hotfix_dictionary/
dictionary.rs

1use crate::{Component, ComponentData, Datatype, DatatypeData, Field, FieldData};
2
3use crate::error::ParseError;
4use crate::message_definition::{MessageData, MessageDefinition};
5use crate::quickfix::QuickFixReader;
6use crate::string::SmartString;
7use fnv::FnvHashMap;
8
9/// Specifies business semantics for application-level entities within the FIX
10/// Protocol.
11///
12/// You can rely on [`Dictionary`] for accessing details about
13/// fields, messages, and other abstract entities as defined in the FIX
14/// specifications. Examples of such information include:
15///
16/// - The mapping of FIX field names to numeric tags (e.g. `BeginString` is 8).
17/// - Which FIX fields are mandatory and which are optional.
18/// - The data type of each and every FIX field.
19/// - What fields to expect in FIX headers.
20///
21/// N.B. The FIX Protocol mandates separation of concerns between session and
22/// application protocol only for FIX 5.0 and subsequent versions. All FIX
23/// Dictionaries for older versions will also contain information about the
24/// session layer.
25#[derive(Debug, Clone)]
26pub struct Dictionary {
27    pub(crate) version: String,
28
29    pub(crate) data_types_by_name: FnvHashMap<SmartString, DatatypeData>,
30
31    pub(crate) fields_by_tags: FnvHashMap<u32, FieldData>,
32    pub(crate) field_tags_by_name: FnvHashMap<SmartString, u32>,
33
34    pub(crate) components_by_name: FnvHashMap<SmartString, ComponentData>,
35
36    pub(crate) messages_by_msgtype: FnvHashMap<SmartString, MessageData>,
37    pub(crate) message_msgtypes_by_name: FnvHashMap<SmartString, SmartString>,
38}
39
40impl Dictionary {
41    /// Creates a new empty FIX Dictionary named `version`.
42    pub fn new<S: ToString>(version: S) -> Self {
43        Dictionary {
44            version: version.to_string(),
45            data_types_by_name: FnvHashMap::default(),
46            fields_by_tags: FnvHashMap::default(),
47            field_tags_by_name: FnvHashMap::default(),
48            components_by_name: FnvHashMap::default(),
49            messages_by_msgtype: FnvHashMap::default(),
50            message_msgtypes_by_name: FnvHashMap::default(),
51        }
52    }
53
54    /// Attempts to read a QuickFIX-style specification file and convert it into
55    /// a [`Dictionary`].
56    pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseError> {
57        let xml_document =
58            roxmltree::Document::parse(input).map_err(|_| ParseError::InvalidFormat)?;
59        QuickFixReader::new(&xml_document)
60    }
61
62    /// Returns the version string associated with this [`Dictionary`] (e.g.
63    /// `FIXT.1.1`, `FIX.4.2`).
64    ///
65    /// ```
66    /// use hotfix_dictionary::Dictionary;
67    ///
68    /// let dict = Dictionary::fix44();
69    /// assert_eq!(dict.version(), "FIX.4.4");
70    /// ```
71    pub fn version(&self) -> &str {
72        self.version.as_str()
73    }
74
75    pub fn load_from_file(path: &str) -> Result<Self, ParseError> {
76        let spec = std::fs::read_to_string(path)
77            .unwrap_or_else(|_| panic!("unable to read FIX dictionary file at {path}"));
78        Dictionary::from_quickfix_spec(&spec)
79    }
80
81    /// Creates a new [`Dictionary`] for FIX 4.0.
82    #[cfg(feature = "fix40")]
83    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix40")))]
84    pub fn fix40() -> Self {
85        let spec = include_str!("resources/quickfix/FIX-4.0.xml");
86        Dictionary::from_quickfix_spec(spec).unwrap()
87    }
88
89    /// Creates a new [`Dictionary`] for FIX 4.1.
90    #[cfg(feature = "fix41")]
91    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix41")))]
92    pub fn fix41() -> Self {
93        let spec = include_str!("resources/quickfix/FIX-4.1.xml");
94        Dictionary::from_quickfix_spec(spec).unwrap()
95    }
96
97    /// Creates a new [`Dictionary`] for FIX 4.2.
98    #[cfg(feature = "fix42")]
99    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix42")))]
100    pub fn fix42() -> Self {
101        let spec = include_str!("resources/quickfix/FIX-4.2.xml");
102        Dictionary::from_quickfix_spec(spec).unwrap()
103    }
104
105    /// Creates a new [`Dictionary`] for FIX 4.3.
106    #[cfg(feature = "fix43")]
107    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix43")))]
108    pub fn fix43() -> Self {
109        let spec = include_str!("resources/quickfix/FIX-4.3.xml");
110        Dictionary::from_quickfix_spec(spec).unwrap()
111    }
112
113    /// Creates a new [`Dictionary`] for FIX 4.4.
114    #[cfg(feature = "fix44")]
115    pub fn fix44() -> Self {
116        let spec = include_str!("resources/quickfix/FIX-4.4.xml");
117        Dictionary::from_quickfix_spec(spec).unwrap()
118    }
119
120    /// Creates a new [`Dictionary`] for FIX 5.0.
121    #[cfg(feature = "fix50")]
122    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix50")))]
123    pub fn fix50() -> Self {
124        let spec = include_str!("resources/quickfix/FIX-5.0.xml");
125        Dictionary::from_quickfix_spec(spec).unwrap()
126    }
127
128    /// Creates a new [`Dictionary`] for FIX 5.0 SP1.
129    #[cfg(feature = "fix50sp1")]
130    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix50sp1")))]
131    pub fn fix50sp1() -> Self {
132        let spec = include_str!("resources/quickfix/FIX-5.0-SP1.xml");
133        Dictionary::from_quickfix_spec(spec).unwrap()
134    }
135
136    /// Creates a new [`Dictionary`] for FIX 5.0 SP2.
137    #[cfg(feature = "fix50sp2")]
138    #[cfg_attr(doc_cfg, doc(cfg(feature = "fix50sp1")))]
139    pub fn fix50sp2() -> Self {
140        let spec = include_str!("resources/quickfix/FIX-5.0-SP2.xml");
141        Dictionary::from_quickfix_spec(spec).unwrap()
142    }
143
144    /// Creates a new [`Dictionary`] for FIXT 1.1.
145    #[cfg(feature = "fixt11")]
146    #[cfg_attr(doc_cfg, doc(cfg(feature = "fixt11")))]
147    pub fn fixt11() -> Self {
148        let spec = include_str!("resources/quickfix/FIXT-1.1.xml");
149        Dictionary::from_quickfix_spec(spec).unwrap()
150    }
151
152    /// Returns a [`Vec`] of FIX [`Dictionary`]'s for the most common FIX
153    /// versions (that have been enabled via feature flags). This is only
154    /// intended for testing purposes.
155    pub fn common_dictionaries() -> Vec<Dictionary> {
156        vec![
157            #[cfg(feature = "fix40")]
158            Self::fix40(),
159            #[cfg(feature = "fix41")]
160            Self::fix41(),
161            #[cfg(feature = "fix42")]
162            Self::fix42(),
163            #[cfg(feature = "fix43")]
164            Self::fix43(),
165            #[cfg(feature = "fix44")]
166            Self::fix44(),
167            #[cfg(feature = "fix50")]
168            Self::fix50(),
169            #[cfg(feature = "fix50sp1")]
170            Self::fix50sp1(),
171            #[cfg(feature = "fix50sp2")]
172            Self::fix50sp2(),
173            #[cfg(feature = "fixt11")]
174            Self::fixt11(),
175        ]
176    }
177
178    /// Returns the [`Message`] associated with `name`, if any.
179    ///
180    /// ```
181    /// use hotfix_dictionary::Dictionary;
182    ///
183    /// let dict = Dictionary::fix44();
184    ///
185    /// let msg1 = dict.message_by_name("Heartbeat").unwrap();
186    /// let msg2 = dict.message_by_msgtype("0").unwrap();
187    /// assert_eq!(msg1.name(), msg2.name());
188    /// ```
189    pub fn message_by_name(&self, name: &str) -> Option<MessageDefinition<'_>> {
190        let msg_type = self.message_msgtypes_by_name.get(name)?;
191        self.message_by_msgtype(msg_type)
192    }
193
194    /// Returns the [`Message`] that has the given `msgtype`, if any.
195    ///
196    /// ```
197    /// use hotfix_dictionary::Dictionary;
198    ///
199    /// let dict = Dictionary::fix44();
200    ///
201    /// let msg1 = dict.message_by_msgtype("0").unwrap();
202    /// let msg2 = dict.message_by_name("Heartbeat").unwrap();
203    /// assert_eq!(msg1.name(), msg2.name());
204    /// ```
205    pub fn message_by_msgtype(&self, msgtype: &str) -> Option<MessageDefinition<'_>> {
206        self.messages_by_msgtype
207            .get(msgtype)
208            .map(|data| MessageDefinition(self, data))
209    }
210
211    /// Returns the [`Component`] named `name`, if any.
212    pub fn component_by_name(&self, name: &str) -> Option<Component<'_>> {
213        self.components_by_name
214            .get(name)
215            .map(|data| Component(self, data))
216    }
217
218    /// Returns the [`Datatype`] named `name`, if any.
219    ///
220    /// ```
221    /// use hotfix_dictionary::Dictionary;
222    ///
223    /// let dict = Dictionary::fix44();
224    /// let dt = dict.datatype_by_name("String").unwrap();
225    /// assert_eq!(dt.name(), "String");
226    /// ```
227    pub fn datatype_by_name(&self, name: &str) -> Option<Datatype<'_>> {
228        self.data_types_by_name
229            .get(name)
230            .map(|data| Datatype(self, data))
231    }
232
233    /// Returns the [`Field`] associated with `tag`, if any.
234    ///
235    /// ```
236    /// use hotfix_dictionary::Dictionary;
237    ///
238    /// let dict = Dictionary::fix44();
239    /// let field1 = dict.field_by_tag(112).unwrap();
240    /// let field2 = dict.field_by_name("TestReqID").unwrap();
241    /// assert_eq!(field1.name(), field2.name());
242    /// ```
243    pub fn field_by_tag(&self, tag: u32) -> Option<Field<'_>> {
244        self.fields_by_tags
245            .get(&tag)
246            .map(|data| Field::new(self, data))
247    }
248
249    /// Returns the [`Field`] named `name`, if any.
250    pub fn field_by_name(&self, name: &str) -> Option<Field<'_>> {
251        let tag = self.field_tags_by_name.get(name)?;
252        self.field_by_tag(*tag)
253    }
254
255    /// Returns a [`Vec`] of all [`Datatype`]'s in this [`Dictionary`]. The ordering
256    /// of items is not specified.
257    ///
258    /// ```
259    /// use hotfix_dictionary::Dictionary;
260    ///
261    /// let dict = Dictionary::fix44();
262    /// // FIX 4.4 defines 23 (FIXME) datatypes.
263    /// assert_eq!(dict.datatypes().len(), 23);
264    /// ```
265    pub fn datatypes(&self) -> Vec<Datatype<'_>> {
266        self.data_types_by_name
267            .values()
268            .map(|data| Datatype(self, data))
269            .collect()
270    }
271
272    /// Returns a [`Vec`] of all [`Message`]'s in this [`Dictionary`]. The ordering
273    /// of items is not specified.
274    ///
275    /// ```
276    /// use hotfix_dictionary::Dictionary;
277    ///
278    /// let dict = Dictionary::fix44();
279    /// let msgs = dict.messages();
280    /// let msg = msgs.iter().find(|m| m.name() == "MarketDataRequest");
281    /// assert_eq!(msg.unwrap().msg_type(), "V");
282    /// ```
283    pub fn messages(&self) -> Vec<MessageDefinition<'_>> {
284        self.messages_by_msgtype
285            .values()
286            .map(|data| MessageDefinition(self, data))
287            .collect()
288    }
289
290    /// Returns a [`Vec`] of all [`Field`]'s in this [`Dictionary`]. The ordering
291    /// of items is not specified.
292    pub fn fields(&self) -> Vec<Field<'_>> {
293        self.fields_by_tags
294            .values()
295            .map(|data| Field::new(self, data))
296            .collect()
297    }
298
299    /// Returns a [`Vec`] of all [`Component`]'s in this [`Dictionary`]. The ordering
300    /// of items is not specified.
301    pub fn components(&self) -> Vec<Component<'_>> {
302        self.components_by_name
303            .values()
304            .map(|data| Component(self, data))
305            .collect()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_load_from_file_success() {
315        let path = concat!(
316            env!("CARGO_MANIFEST_DIR"),
317            "/src/resources/quickfix/FIX-4.4.xml"
318        );
319        let dict = Dictionary::load_from_file(path).unwrap();
320        assert_eq!(dict.version(), "FIX.4.4");
321        assert!(dict.message_by_name("Heartbeat").is_some());
322    }
323
324    #[test]
325    fn test_load_from_file_invalid_content() {
326        let path = concat!(
327            env!("CARGO_MANIFEST_DIR"),
328            "/src/test_data/quickfix_specs/empty_file.xml"
329        );
330        let result = Dictionary::load_from_file(path);
331        assert!(result.is_err());
332    }
333}