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