use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Version {
Fix40,
Fix41,
Fix42,
Fix43,
Fix44,
Fix50,
Fix50Sp1,
Fix50Sp2,
Fixt11,
}
impl Version {
#[must_use]
pub const fn begin_string(&self) -> &'static str {
match self {
Self::Fix40 => "FIX.4.0",
Self::Fix41 => "FIX.4.1",
Self::Fix42 => "FIX.4.2",
Self::Fix43 => "FIX.4.3",
Self::Fix44 => "FIX.4.4",
Self::Fix50 | Self::Fix50Sp1 | Self::Fix50Sp2 | Self::Fixt11 => "FIXT.1.1",
}
}
#[must_use]
pub const fn appl_ver_id(&self) -> Option<&'static str> {
match self {
Self::Fix50 => Some("7"),
Self::Fix50Sp1 => Some("8"),
Self::Fix50Sp2 => Some("9"),
_ => None,
}
}
#[must_use]
pub const fn uses_fixt(&self) -> bool {
matches!(
self,
Self::Fix50 | Self::Fix50Sp1 | Self::Fix50Sp2 | Self::Fixt11
)
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.begin_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FieldType {
Int,
Length,
SeqNum,
NumInGroup,
TagNum,
DayOfMonth,
Float,
Qty,
Price,
PriceOffset,
Amt,
Percentage,
Char,
Boolean,
String,
MultipleCharValue,
MultipleStringValue,
Country,
Currency,
Exchange,
MonthYear,
UtcTimestamp,
UtcTimeOnly,
UtcDateOnly,
LocalMktDate,
LocalMktTime,
TzTimeOnly,
TzTimestamp,
Data,
XmlData,
Language,
Pattern,
Tenor,
Reserved,
}
impl FieldType {
#[must_use]
pub const fn is_numeric(&self) -> bool {
matches!(
self,
Self::Int
| Self::Length
| Self::SeqNum
| Self::NumInGroup
| Self::TagNum
| Self::DayOfMonth
| Self::Float
| Self::Qty
| Self::Price
| Self::PriceOffset
| Self::Amt
| Self::Percentage
)
}
}
impl std::str::FromStr for FieldType {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_uppercase().as_str() {
"INT" => Self::Int,
"LENGTH" => Self::Length,
"SEQNUM" => Self::SeqNum,
"NUMINGROUP" => Self::NumInGroup,
"TAGNUM" => Self::TagNum,
"DAYOFMONTH" => Self::DayOfMonth,
"FLOAT" => Self::Float,
"QTY" | "QUANTITY" => Self::Qty,
"PRICE" => Self::Price,
"PRICEOFFSET" => Self::PriceOffset,
"AMT" | "AMOUNT" => Self::Amt,
"PERCENTAGE" => Self::Percentage,
"CHAR" => Self::Char,
"BOOLEAN" => Self::Boolean,
"STRING" => Self::String,
"MULTIPLECHARVALUE" => Self::MultipleCharValue,
"MULTIPLESTRINGVALUE" => Self::MultipleStringValue,
"COUNTRY" => Self::Country,
"CURRENCY" => Self::Currency,
"EXCHANGE" => Self::Exchange,
"MONTHYEAR" => Self::MonthYear,
"UTCTIMESTAMP" => Self::UtcTimestamp,
"UTCTIMEONLY" => Self::UtcTimeOnly,
"UTCDATEONLY" => Self::UtcDateOnly,
"LOCALMKTDATE" => Self::LocalMktDate,
"LOCALMKTTIME" => Self::LocalMktTime,
"TZTIMEONLY" => Self::TzTimeOnly,
"TZTIMESTAMP" => Self::TzTimestamp,
"DATA" => Self::Data,
"XMLDATA" => Self::XmlData,
"LANGUAGE" => Self::Language,
"PATTERN" => Self::Pattern,
"TENOR" => Self::Tenor,
_ => Self::String,
})
}
}
impl FieldType {
#[must_use]
pub const fn is_timestamp(&self) -> bool {
matches!(
self,
Self::UtcTimestamp
| Self::UtcTimeOnly
| Self::UtcDateOnly
| Self::LocalMktDate
| Self::LocalMktTime
| Self::TzTimeOnly
| Self::TzTimestamp
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDef {
pub tag: u32,
pub name: String,
pub field_type: FieldType,
pub values: Option<HashMap<String, String>>,
pub description: Option<String>,
}
impl FieldDef {
#[must_use]
pub fn new(tag: u32, name: impl Into<String>, field_type: FieldType) -> Self {
Self {
tag,
name: name.into(),
field_type,
values: None,
description: None,
}
}
#[must_use]
pub fn with_values(mut self, values: HashMap<String, String>) -> Self {
self.values = Some(values);
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldRef {
pub tag: u32,
pub name: String,
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupDef {
pub count_tag: u32,
pub name: String,
pub delimiter_tag: u32,
pub fields: Vec<FieldRef>,
pub groups: Vec<GroupDef>,
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentDef {
pub name: String,
pub fields: Vec<FieldRef>,
pub groups: Vec<GroupDef>,
pub components: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageDef {
pub msg_type: String,
pub name: String,
pub category: MessageCategory,
pub fields: Vec<FieldRef>,
pub groups: Vec<GroupDef>,
pub components: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageCategory {
Admin,
App,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dictionary {
pub version: Version,
pub fields: HashMap<u32, FieldDef>,
pub fields_by_name: HashMap<String, u32>,
pub messages: HashMap<String, MessageDef>,
pub components: HashMap<String, ComponentDef>,
pub header: Vec<FieldRef>,
pub trailer: Vec<FieldRef>,
}
impl Dictionary {
#[must_use]
pub fn new(version: Version) -> Self {
Self {
version,
fields: HashMap::new(),
fields_by_name: HashMap::new(),
messages: HashMap::new(),
components: HashMap::new(),
header: Vec::new(),
trailer: Vec::new(),
}
}
pub fn add_field(&mut self, field: FieldDef) {
self.fields_by_name.insert(field.name.clone(), field.tag);
self.fields.insert(field.tag, field);
}
pub fn add_message(&mut self, message: MessageDef) {
self.messages.insert(message.msg_type.clone(), message);
}
pub fn add_component(&mut self, component: ComponentDef) {
self.components.insert(component.name.clone(), component);
}
#[must_use]
pub fn get_field(&self, tag: u32) -> Option<&FieldDef> {
self.fields.get(&tag)
}
#[must_use]
pub fn get_field_by_name(&self, name: &str) -> Option<&FieldDef> {
self.fields_by_name
.get(name)
.and_then(|tag| self.fields.get(tag))
}
#[must_use]
pub fn get_message(&self, msg_type: &str) -> Option<&MessageDef> {
self.messages.get(msg_type)
}
#[must_use]
pub fn get_component(&self, name: &str) -> Option<&ComponentDef> {
self.components.get(name)
}
pub fn fields(&self) -> impl Iterator<Item = &FieldDef> {
self.fields.values()
}
pub fn messages(&self) -> impl Iterator<Item = &MessageDef> {
self.messages.values()
}
pub fn components(&self) -> impl Iterator<Item = &ComponentDef> {
self.components.values()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_begin_string() {
assert_eq!(Version::Fix42.begin_string(), "FIX.4.2");
assert_eq!(Version::Fix44.begin_string(), "FIX.4.4");
assert_eq!(Version::Fix50Sp2.begin_string(), "FIXT.1.1");
}
#[test]
fn test_version_appl_ver_id() {
assert_eq!(Version::Fix44.appl_ver_id(), None);
assert_eq!(Version::Fix50.appl_ver_id(), Some("7"));
assert_eq!(Version::Fix50Sp2.appl_ver_id(), Some("9"));
}
#[test]
fn test_field_type_from_str() {
assert_eq!("INT".parse::<FieldType>().unwrap(), FieldType::Int);
assert_eq!("STRING".parse::<FieldType>().unwrap(), FieldType::String);
assert_eq!(
"UTCTIMESTAMP".parse::<FieldType>().unwrap(),
FieldType::UtcTimestamp
);
assert_eq!("unknown".parse::<FieldType>().unwrap(), FieldType::String);
}
#[test]
fn test_field_type_is_numeric() {
assert!(FieldType::Int.is_numeric());
assert!(FieldType::Price.is_numeric());
assert!(!FieldType::String.is_numeric());
}
#[test]
fn test_dictionary_field_operations() {
let mut dict = Dictionary::new(Version::Fix44);
let field = FieldDef::new(35, "MsgType", FieldType::String);
dict.add_field(field);
assert!(dict.get_field(35).is_some());
assert!(dict.get_field_by_name("MsgType").is_some());
assert!(dict.get_field(999).is_none());
}
}