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