Skip to main content

ironfix_dictionary/
schema.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! Schema definitions for FIX dictionaries.
8//!
9//! This module defines the structures that represent FIX protocol specifications:
10//! - [`FieldDef`]: Field definitions with tag, name, and type
11//! - [`MessageDef`]: Message definitions with required/optional fields
12//! - [`ComponentDef`]: Reusable component definitions
13//! - [`GroupDef`]: Repeating group definitions
14//! - [`Dictionary`]: Complete FIX version dictionary
15
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19/// FIX protocol version.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum Version {
22    /// FIX 4.0
23    Fix40,
24    /// FIX 4.1
25    Fix41,
26    /// FIX 4.2
27    Fix42,
28    /// FIX 4.3
29    Fix43,
30    /// FIX 4.4
31    Fix44,
32    /// FIX 5.0
33    Fix50,
34    /// FIX 5.0 SP1
35    Fix50Sp1,
36    /// FIX 5.0 SP2
37    Fix50Sp2,
38    /// FIXT 1.1 (transport layer for FIX 5.0+)
39    Fixt11,
40}
41
42impl Version {
43    /// Returns the BeginString value for this version.
44    #[must_use]
45    pub const fn begin_string(&self) -> &'static str {
46        match self {
47            Self::Fix40 => "FIX.4.0",
48            Self::Fix41 => "FIX.4.1",
49            Self::Fix42 => "FIX.4.2",
50            Self::Fix43 => "FIX.4.3",
51            Self::Fix44 => "FIX.4.4",
52            Self::Fix50 | Self::Fix50Sp1 | Self::Fix50Sp2 | Self::Fixt11 => "FIXT.1.1",
53        }
54    }
55
56    /// Returns the ApplVerID for FIX 5.0+ versions.
57    #[must_use]
58    pub const fn appl_ver_id(&self) -> Option<&'static str> {
59        match self {
60            Self::Fix50 => Some("7"),
61            Self::Fix50Sp1 => Some("8"),
62            Self::Fix50Sp2 => Some("9"),
63            _ => None,
64        }
65    }
66
67    /// Returns true if this version uses FIXT transport.
68    #[must_use]
69    pub const fn uses_fixt(&self) -> bool {
70        matches!(
71            self,
72            Self::Fix50 | Self::Fix50Sp1 | Self::Fix50Sp2 | Self::Fixt11
73        )
74    }
75}
76
77impl std::fmt::Display for Version {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(f, "{}", self.begin_string())
80    }
81}
82
83/// FIX field data type.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
85pub enum FieldType {
86    /// Integer value.
87    Int,
88    /// Length field (for data fields).
89    Length,
90    /// Sequence number.
91    SeqNum,
92    /// Number of entries in a repeating group.
93    NumInGroup,
94    /// Tag number reference.
95    TagNum,
96    /// Day of month (1-31).
97    DayOfMonth,
98    /// Floating point number.
99    Float,
100    /// Quantity.
101    Qty,
102    /// Price.
103    Price,
104    /// Price offset.
105    PriceOffset,
106    /// Amount (price * quantity).
107    Amt,
108    /// Percentage.
109    Percentage,
110    /// Single character.
111    Char,
112    /// Boolean (Y/N).
113    Boolean,
114    /// String.
115    String,
116    /// Multiple character value (space-separated).
117    MultipleCharValue,
118    /// Multiple string value (space-separated).
119    MultipleStringValue,
120    /// Country code (ISO 3166).
121    Country,
122    /// Currency code (ISO 4217).
123    Currency,
124    /// Exchange code (ISO 10383 MIC).
125    Exchange,
126    /// Month-year (YYYYMM or YYYYMMDD or YYYYMMWW).
127    MonthYear,
128    /// UTC timestamp.
129    UtcTimestamp,
130    /// UTC time only.
131    UtcTimeOnly,
132    /// UTC date only.
133    UtcDateOnly,
134    /// Local market date.
135    LocalMktDate,
136    /// Local market time.
137    LocalMktTime,
138    /// Timezone.
139    TzTimeOnly,
140    /// Timezone with timestamp.
141    TzTimestamp,
142    /// Raw data (binary).
143    Data,
144    /// XML data.
145    XmlData,
146    /// Language code (ISO 639-1).
147    Language,
148    /// Pattern (regex).
149    Pattern,
150    /// Tenor (e.g., "1M", "3M").
151    Tenor,
152    /// Reserved for future use.
153    Reserved,
154}
155
156impl FieldType {
157    /// Returns true if this type represents a numeric value.
158    #[must_use]
159    pub const fn is_numeric(&self) -> bool {
160        matches!(
161            self,
162            Self::Int
163                | Self::Length
164                | Self::SeqNum
165                | Self::NumInGroup
166                | Self::TagNum
167                | Self::DayOfMonth
168                | Self::Float
169                | Self::Qty
170                | Self::Price
171                | Self::PriceOffset
172                | Self::Amt
173                | Self::Percentage
174        )
175    }
176}
177
178impl std::str::FromStr for FieldType {
179    type Err = std::convert::Infallible;
180
181    /// Creates a FieldType from a string name.
182    ///
183    /// # Arguments
184    /// * `s` - The type name from the FIX dictionary
185    fn from_str(s: &str) -> Result<Self, Self::Err> {
186        Ok(match s.to_uppercase().as_str() {
187            "INT" => Self::Int,
188            "LENGTH" => Self::Length,
189            "SEQNUM" => Self::SeqNum,
190            "NUMINGROUP" => Self::NumInGroup,
191            "TAGNUM" => Self::TagNum,
192            "DAYOFMONTH" => Self::DayOfMonth,
193            "FLOAT" => Self::Float,
194            "QTY" | "QUANTITY" => Self::Qty,
195            "PRICE" => Self::Price,
196            "PRICEOFFSET" => Self::PriceOffset,
197            "AMT" | "AMOUNT" => Self::Amt,
198            "PERCENTAGE" => Self::Percentage,
199            "CHAR" => Self::Char,
200            "BOOLEAN" => Self::Boolean,
201            "STRING" => Self::String,
202            "MULTIPLECHARVALUE" => Self::MultipleCharValue,
203            "MULTIPLESTRINGVALUE" => Self::MultipleStringValue,
204            "COUNTRY" => Self::Country,
205            "CURRENCY" => Self::Currency,
206            "EXCHANGE" => Self::Exchange,
207            "MONTHYEAR" => Self::MonthYear,
208            "UTCTIMESTAMP" => Self::UtcTimestamp,
209            "UTCTIMEONLY" => Self::UtcTimeOnly,
210            "UTCDATEONLY" => Self::UtcDateOnly,
211            "LOCALMKTDATE" => Self::LocalMktDate,
212            "LOCALMKTTIME" => Self::LocalMktTime,
213            "TZTIMEONLY" => Self::TzTimeOnly,
214            "TZTIMESTAMP" => Self::TzTimestamp,
215            "DATA" => Self::Data,
216            "XMLDATA" => Self::XmlData,
217            "LANGUAGE" => Self::Language,
218            "PATTERN" => Self::Pattern,
219            "TENOR" => Self::Tenor,
220            _ => Self::String,
221        })
222    }
223}
224
225impl FieldType {
226    /// Returns true if this type represents a timestamp.
227    #[must_use]
228    pub const fn is_timestamp(&self) -> bool {
229        matches!(
230            self,
231            Self::UtcTimestamp
232                | Self::UtcTimeOnly
233                | Self::UtcDateOnly
234                | Self::LocalMktDate
235                | Self::LocalMktTime
236                | Self::TzTimeOnly
237                | Self::TzTimestamp
238        )
239    }
240}
241
242/// Definition of a FIX field.
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct FieldDef {
245    /// Field tag number.
246    pub tag: u32,
247    /// Field name.
248    pub name: String,
249    /// Field data type.
250    pub field_type: FieldType,
251    /// Valid values for enumerated fields.
252    pub values: Option<HashMap<String, String>>,
253    /// Field description.
254    pub description: Option<String>,
255}
256
257impl FieldDef {
258    /// Creates a new field definition.
259    ///
260    /// # Arguments
261    /// * `tag` - The field tag number
262    /// * `name` - The field name
263    /// * `field_type` - The field data type
264    #[must_use]
265    pub fn new(tag: u32, name: impl Into<String>, field_type: FieldType) -> Self {
266        Self {
267            tag,
268            name: name.into(),
269            field_type,
270            values: None,
271            description: None,
272        }
273    }
274
275    /// Adds valid values for an enumerated field.
276    #[must_use]
277    pub fn with_values(mut self, values: HashMap<String, String>) -> Self {
278        self.values = Some(values);
279        self
280    }
281
282    /// Adds a description.
283    #[must_use]
284    pub fn with_description(mut self, description: impl Into<String>) -> Self {
285        self.description = Some(description.into());
286        self
287    }
288}
289
290/// Reference to a field within a message or component.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct FieldRef {
293    /// Field tag number.
294    pub tag: u32,
295    /// Field name.
296    pub name: String,
297    /// Whether the field is required.
298    pub required: bool,
299}
300
301/// Definition of a repeating group.
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct GroupDef {
304    /// Tag of the count field (NumInGroup).
305    pub count_tag: u32,
306    /// Name of the group.
307    pub name: String,
308    /// Tag of the first field in each group entry (delimiter).
309    pub delimiter_tag: u32,
310    /// Fields within each group entry.
311    pub fields: Vec<FieldRef>,
312    /// Nested groups within this group.
313    pub groups: Vec<GroupDef>,
314    /// Whether the group is required.
315    pub required: bool,
316}
317
318/// Definition of a reusable component.
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ComponentDef {
321    /// Component name.
322    pub name: String,
323    /// Fields in this component.
324    pub fields: Vec<FieldRef>,
325    /// Groups in this component.
326    pub groups: Vec<GroupDef>,
327    /// Nested components.
328    pub components: Vec<String>,
329}
330
331/// Definition of a FIX message.
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct MessageDef {
334    /// Message type value (tag 35).
335    pub msg_type: String,
336    /// Message name.
337    pub name: String,
338    /// Message category (admin or app).
339    pub category: MessageCategory,
340    /// Fields in this message.
341    pub fields: Vec<FieldRef>,
342    /// Groups in this message.
343    pub groups: Vec<GroupDef>,
344    /// Components used in this message.
345    pub components: Vec<String>,
346}
347
348/// Message category.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
350pub enum MessageCategory {
351    /// Administrative message (session level).
352    Admin,
353    /// Application message.
354    App,
355}
356
357/// Complete FIX dictionary for a specific version.
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct Dictionary {
360    /// FIX version.
361    pub version: Version,
362    /// Field definitions indexed by tag.
363    pub fields: HashMap<u32, FieldDef>,
364    /// Field definitions indexed by name.
365    pub fields_by_name: HashMap<String, u32>,
366    /// Message definitions indexed by msg_type.
367    pub messages: HashMap<String, MessageDef>,
368    /// Component definitions indexed by name.
369    pub components: HashMap<String, ComponentDef>,
370    /// Header fields.
371    pub header: Vec<FieldRef>,
372    /// Trailer fields.
373    pub trailer: Vec<FieldRef>,
374}
375
376impl Dictionary {
377    /// Creates a new empty dictionary for the specified version.
378    ///
379    /// # Arguments
380    /// * `version` - The FIX version
381    #[must_use]
382    pub fn new(version: Version) -> Self {
383        Self {
384            version,
385            fields: HashMap::new(),
386            fields_by_name: HashMap::new(),
387            messages: HashMap::new(),
388            components: HashMap::new(),
389            header: Vec::new(),
390            trailer: Vec::new(),
391        }
392    }
393
394    /// Adds a field definition.
395    pub fn add_field(&mut self, field: FieldDef) {
396        self.fields_by_name.insert(field.name.clone(), field.tag);
397        self.fields.insert(field.tag, field);
398    }
399
400    /// Adds a message definition.
401    pub fn add_message(&mut self, message: MessageDef) {
402        self.messages.insert(message.msg_type.clone(), message);
403    }
404
405    /// Adds a component definition.
406    pub fn add_component(&mut self, component: ComponentDef) {
407        self.components.insert(component.name.clone(), component);
408    }
409
410    /// Gets a field definition by tag.
411    #[must_use]
412    pub fn get_field(&self, tag: u32) -> Option<&FieldDef> {
413        self.fields.get(&tag)
414    }
415
416    /// Gets a field definition by name.
417    #[must_use]
418    pub fn get_field_by_name(&self, name: &str) -> Option<&FieldDef> {
419        self.fields_by_name
420            .get(name)
421            .and_then(|tag| self.fields.get(tag))
422    }
423
424    /// Gets a message definition by type.
425    #[must_use]
426    pub fn get_message(&self, msg_type: &str) -> Option<&MessageDef> {
427        self.messages.get(msg_type)
428    }
429
430    /// Gets a component definition by name.
431    #[must_use]
432    pub fn get_component(&self, name: &str) -> Option<&ComponentDef> {
433        self.components.get(name)
434    }
435
436    /// Returns an iterator over all field definitions.
437    pub fn fields(&self) -> impl Iterator<Item = &FieldDef> {
438        self.fields.values()
439    }
440
441    /// Returns an iterator over all message definitions.
442    pub fn messages(&self) -> impl Iterator<Item = &MessageDef> {
443        self.messages.values()
444    }
445
446    /// Returns an iterator over all component definitions.
447    pub fn components(&self) -> impl Iterator<Item = &ComponentDef> {
448        self.components.values()
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_version_begin_string() {
458        assert_eq!(Version::Fix42.begin_string(), "FIX.4.2");
459        assert_eq!(Version::Fix44.begin_string(), "FIX.4.4");
460        assert_eq!(Version::Fix50Sp2.begin_string(), "FIXT.1.1");
461    }
462
463    #[test]
464    fn test_version_appl_ver_id() {
465        assert_eq!(Version::Fix44.appl_ver_id(), None);
466        assert_eq!(Version::Fix50.appl_ver_id(), Some("7"));
467        assert_eq!(Version::Fix50Sp2.appl_ver_id(), Some("9"));
468    }
469
470    #[test]
471    fn test_field_type_from_str() {
472        assert_eq!("INT".parse::<FieldType>().unwrap(), FieldType::Int);
473        assert_eq!("STRING".parse::<FieldType>().unwrap(), FieldType::String);
474        assert_eq!(
475            "UTCTIMESTAMP".parse::<FieldType>().unwrap(),
476            FieldType::UtcTimestamp
477        );
478        assert_eq!("unknown".parse::<FieldType>().unwrap(), FieldType::String);
479    }
480
481    #[test]
482    fn test_field_type_is_numeric() {
483        assert!(FieldType::Int.is_numeric());
484        assert!(FieldType::Price.is_numeric());
485        assert!(!FieldType::String.is_numeric());
486    }
487
488    #[test]
489    fn test_dictionary_field_operations() {
490        let mut dict = Dictionary::new(Version::Fix44);
491        let field = FieldDef::new(35, "MsgType", FieldType::String);
492        dict.add_field(field);
493
494        assert!(dict.get_field(35).is_some());
495        assert!(dict.get_field_by_name("MsgType").is_some());
496        assert!(dict.get_field(999).is_none());
497    }
498}