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