Skip to main content

quickfix_tokio/
value.rs

1//! Conversions between Rust types and FIX wire representations of field values.
2
3use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone, Utc};
4
5use crate::error::ConversionError;
6use crate::message::Tag;
7
8/// A value that can be written as a FIX field value.
9pub trait FixEncode {
10    fn encode(&self, buf: &mut Vec<u8>);
11}
12
13/// A value that can be parsed from a FIX field value.
14pub trait FixDecode: Sized {
15    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError>;
16}
17
18fn invalid(tag: Tag, bytes: &[u8]) -> ConversionError {
19    ConversionError::InvalidValue { tag, value: String::from_utf8_lossy(bytes).into_owned() }
20}
21
22impl FixEncode for &str {
23    fn encode(&self, buf: &mut Vec<u8>) {
24        buf.extend_from_slice(self.as_bytes());
25    }
26}
27
28impl FixEncode for String {
29    fn encode(&self, buf: &mut Vec<u8>) {
30        buf.extend_from_slice(self.as_bytes());
31    }
32}
33
34impl FixEncode for &[u8] {
35    fn encode(&self, buf: &mut Vec<u8>) {
36        buf.extend_from_slice(self);
37    }
38}
39
40impl FixEncode for Vec<u8> {
41    fn encode(&self, buf: &mut Vec<u8>) {
42        buf.extend_from_slice(self);
43    }
44}
45
46impl FixEncode for char {
47    fn encode(&self, buf: &mut Vec<u8>) {
48        let mut b = [0u8; 4];
49        buf.extend_from_slice(self.encode_utf8(&mut b).as_bytes());
50    }
51}
52
53impl FixEncode for bool {
54    fn encode(&self, buf: &mut Vec<u8>) {
55        buf.push(if *self { b'Y' } else { b'N' });
56    }
57}
58
59macro_rules! impl_int {
60    ($($t:ty),*) => {$(
61        impl FixEncode for $t {
62            fn encode(&self, buf: &mut Vec<u8>) {
63                buf.extend_from_slice(itoa_buf(*self as i64).as_bytes());
64            }
65        }
66        impl FixDecode for $t {
67            fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
68                let s = std::str::from_utf8(bytes).map_err(|_| invalid(tag, bytes))?;
69                s.parse::<$t>().map_err(|_| invalid(tag, bytes))
70            }
71        }
72    )*};
73}
74impl_int!(i32, i64, u32, u64, usize);
75
76fn itoa_buf(v: i64) -> String {
77    v.to_string()
78}
79
80impl FixEncode for f64 {
81    fn encode(&self, buf: &mut Vec<u8>) {
82        // FIX float: decimal notation, no exponent.
83        let s = format!("{}", self);
84        debug_assert!(!s.contains('e') && !s.contains('E'));
85        buf.extend_from_slice(s.as_bytes());
86    }
87}
88
89impl FixDecode for f64 {
90    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
91        let s = std::str::from_utf8(bytes).map_err(|_| invalid(tag, bytes))?;
92        // Only digits, '.', and a leading '-' are valid on the FIX wire
93        // (no '+', no exponents) — matches the C++ DoubleConvertor.
94        if s.bytes().any(|b| !matches!(b, b'0'..=b'9' | b'.' | b'-')) {
95            return Err(invalid(tag, bytes));
96        }
97        s.parse::<f64>().map_err(|_| invalid(tag, bytes))
98    }
99}
100
101/// Exact fixed-point decimal for FIX float-family fields. Preserves the wire
102/// scale (so `"1.50"` round-trips as `"1.50"`, not `"1.5"`).
103#[cfg(feature = "decimal")]
104impl FixEncode for rust_decimal::Decimal {
105    fn encode(&self, buf: &mut Vec<u8>) {
106        // rust_decimal's Display is always plain decimal (never exponent).
107        use std::fmt::Write;
108        let mut s = String::new();
109        let _ = write!(s, "{self}");
110        buf.extend_from_slice(s.as_bytes());
111    }
112}
113
114#[cfg(feature = "decimal")]
115impl FixDecode for rust_decimal::Decimal {
116    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
117        let s = std::str::from_utf8(bytes).map_err(|_| invalid(tag, bytes))?;
118        // Same wire rules as f64: no '+', no exponents.
119        if s.bytes().any(|b| !matches!(b, b'0'..=b'9' | b'.' | b'-')) {
120            return Err(invalid(tag, bytes));
121        }
122        s.parse::<rust_decimal::Decimal>().map_err(|_| invalid(tag, bytes))
123    }
124}
125
126impl FixDecode for String {
127    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
128        String::from_utf8(bytes.to_vec()).map_err(|_| invalid(tag, bytes))
129    }
130}
131
132impl FixDecode for Vec<u8> {
133    fn decode(_tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
134        Ok(bytes.to_vec())
135    }
136}
137
138impl FixDecode for bool {
139    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
140        match bytes {
141            b"Y" => Ok(true),
142            b"N" => Ok(false),
143            _ => Err(invalid(tag, bytes)),
144        }
145    }
146}
147
148impl FixDecode for char {
149    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
150        let s = std::str::from_utf8(bytes).map_err(|_| invalid(tag, bytes))?;
151        let mut chars = s.chars();
152        match (chars.next(), chars.next()) {
153            (Some(c), None) => Ok(c),
154            _ => Err(invalid(tag, bytes)),
155        }
156    }
157}
158
159/// Precision used when encoding UTCTimestamp values (tag 52 et al).
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
161pub enum TimestampPrecision {
162    Seconds,
163    #[default]
164    Millis,
165    Micros,
166    Nanos,
167}
168
169/// A FIX UTCTimestamp: `YYYYMMDD-HH:MM:SS[.sss[sss[sss]]]`.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct UtcTimestamp {
172    pub time: DateTime<Utc>,
173    pub precision: TimestampPrecision,
174}
175
176impl UtcTimestamp {
177    pub fn now() -> Self {
178        Self { time: Utc::now(), precision: TimestampPrecision::default() }
179    }
180
181    pub fn new(time: DateTime<Utc>, precision: TimestampPrecision) -> Self {
182        Self { time, precision }
183    }
184}
185
186impl FixEncode for UtcTimestamp {
187    fn encode(&self, buf: &mut Vec<u8>) {
188        let fmt = match self.precision {
189            TimestampPrecision::Seconds => "%Y%m%d-%H:%M:%S",
190            TimestampPrecision::Millis => "%Y%m%d-%H:%M:%S%.3f",
191            TimestampPrecision::Micros => "%Y%m%d-%H:%M:%S%.6f",
192            TimestampPrecision::Nanos => "%Y%m%d-%H:%M:%S%.9f",
193        };
194        buf.extend_from_slice(self.time.format(fmt).to_string().as_bytes());
195    }
196}
197
198impl FixDecode for UtcTimestamp {
199    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
200        let s = std::str::from_utf8(bytes).map_err(|_| invalid(tag, bytes))?;
201        let (fmt, precision) = match s.len() {
202            17 => ("%Y%m%d-%H:%M:%S", TimestampPrecision::Seconds),
203            21 => ("%Y%m%d-%H:%M:%S%.3f", TimestampPrecision::Millis),
204            24 => ("%Y%m%d-%H:%M:%S%.6f", TimestampPrecision::Micros),
205            27 => ("%Y%m%d-%H:%M:%S%.9f", TimestampPrecision::Nanos),
206            _ => return Err(invalid(tag, bytes)),
207        };
208        let naive = NaiveDateTime::parse_from_str(s, fmt).map_err(|_| invalid(tag, bytes))?;
209        Ok(Self { time: Utc.from_utc_datetime(&naive), precision })
210    }
211}
212
213/// A FIX UTCDateOnly / LocalMktDate: `YYYYMMDD`.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct FixDate(pub NaiveDate);
216
217impl FixEncode for FixDate {
218    fn encode(&self, buf: &mut Vec<u8>) {
219        buf.extend_from_slice(self.0.format("%Y%m%d").to_string().as_bytes());
220    }
221}
222
223impl FixDecode for FixDate {
224    fn decode(tag: Tag, bytes: &[u8]) -> Result<Self, ConversionError> {
225        let s = std::str::from_utf8(bytes).map_err(|_| invalid(tag, bytes))?;
226        NaiveDate::parse_from_str(s, "%Y%m%d").map(FixDate).map_err(|_| invalid(tag, bytes))
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    fn enc(v: impl FixEncode) -> Vec<u8> {
235        let mut buf = Vec::new();
236        v.encode(&mut buf);
237        buf
238    }
239
240    #[test]
241    fn roundtrip_scalars() {
242        assert_eq!(enc(42i64), b"42");
243        assert_eq!(enc(-7i32), b"-7");
244        assert_eq!(enc(true), b"Y");
245        assert_eq!(enc(false), b"N");
246        assert_eq!(enc(1.5f64), b"1.5");
247        assert_eq!(enc("ABC"), b"ABC");
248        assert_eq!(i64::decode(1, b"42").unwrap(), 42);
249        assert!(i64::decode(1, b"4x2").is_err());
250        assert!(bool::decode(1, b"X").is_err());
251        assert!(f64::decode(1, b"1e5").is_err());
252        assert_eq!(f64::decode(1, b"-1.25").unwrap(), -1.25);
253    }
254
255    #[cfg(feature = "decimal")]
256    #[test]
257    fn decimal_exactness_and_scale() {
258        use rust_decimal::Decimal;
259        // Exact arithmetic: 0.1 + 0.2 == 0.3 (f64 would give 0.30000000000000004).
260        let a: Decimal = "0.1".parse().unwrap();
261        let b: Decimal = "0.2".parse().unwrap();
262        assert_eq!(a + b, "0.3".parse::<Decimal>().unwrap());
263
264        // Wire scale is preserved on round-trip.
265        let d = Decimal::decode(44, b"1.50").unwrap();
266        assert_eq!(enc(d), b"1.50");
267        // High-precision value survives (>15 significant digits).
268        let d = Decimal::decode(44, b"123456789.012345678").unwrap();
269        assert_eq!(enc(d), b"123456789.012345678");
270        // Plain integer and negative.
271        assert_eq!(enc(Decimal::decode(38, b"100").unwrap()), b"100");
272        assert_eq!(enc(Decimal::decode(44, b"-1.25").unwrap()), b"-1.25");
273        // Invalid wire formats rejected.
274        assert!(Decimal::decode(44, b"1e5").is_err());
275        assert!(Decimal::decode(44, b"+1.5").is_err());
276    }
277
278    #[test]
279    fn roundtrip_timestamp() {
280        let ts = UtcTimestamp::decode(52, b"20140515-19:49:56.659").unwrap();
281        assert_eq!(ts.precision, TimestampPrecision::Millis);
282        assert_eq!(enc(ts), b"20140515-19:49:56.659");
283
284        let ts = UtcTimestamp::decode(52, b"20140515-19:49:56").unwrap();
285        assert_eq!(ts.precision, TimestampPrecision::Seconds);
286        assert_eq!(enc(ts), b"20140515-19:49:56");
287
288        assert!(UtcTimestamp::decode(52, b"2014-05-15").is_err());
289    }
290}