Skip to main content

ironfix_core/
field.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! Field types and traits for FIX protocol messages.
8//!
9//! This module provides:
10//! - [`FieldTag`]: Type-safe wrapper for FIX field tag numbers
11//! - [`FieldRef`]: Zero-copy reference to a field within a message buffer
12//! - [`FieldValue`]: Enumeration of possible field value types
13//! - [`FixField`]: Trait for typed field access
14
15use crate::error::DecodeError;
16use bytes::Bytes;
17use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use std::str::FromStr;
21
22/// FIX field tag number.
23///
24/// Tags are positive integers that identify fields within a FIX message.
25/// Standard tags are defined in the FIX specification (1-5000 range),
26/// while user-defined tags use the 5001+ range.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
28#[repr(transparent)]
29#[serde(transparent)]
30pub struct FieldTag(u32);
31
32impl FieldTag {
33    /// Creates a new field tag.
34    ///
35    /// # Arguments
36    /// * `tag` - The tag number (must be > 0)
37    #[inline]
38    #[must_use]
39    pub const fn new(tag: u32) -> Self {
40        Self(tag)
41    }
42
43    /// Returns the raw tag number.
44    #[inline]
45    #[must_use]
46    pub const fn value(self) -> u32 {
47        self.0
48    }
49
50    /// Returns true if this is a standard FIX tag (1-5000).
51    #[inline]
52    #[must_use]
53    pub const fn is_standard(self) -> bool {
54        self.0 >= 1 && self.0 <= 5000
55    }
56
57    /// Returns true if this is a user-defined tag (5001+).
58    #[inline]
59    #[must_use]
60    pub const fn is_user_defined(self) -> bool {
61        self.0 > 5000
62    }
63}
64
65impl From<u32> for FieldTag {
66    fn from(tag: u32) -> Self {
67        Self(tag)
68    }
69}
70
71impl From<FieldTag> for u32 {
72    fn from(tag: FieldTag) -> Self {
73        tag.0
74    }
75}
76
77impl fmt::Display for FieldTag {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.0)
80    }
81}
82
83/// Zero-copy reference to a field within a FIX message buffer.
84///
85/// This struct holds references to the original message buffer,
86/// avoiding allocation during parsing.
87#[derive(Debug, Clone, Copy)]
88pub struct FieldRef<'a> {
89    /// The field tag number.
90    pub tag: u32,
91    /// Reference to the field value bytes (without delimiters).
92    pub value: &'a [u8],
93}
94
95impl<'a> FieldRef<'a> {
96    /// Creates a new field reference.
97    ///
98    /// # Arguments
99    /// * `tag` - The field tag number
100    /// * `value` - Reference to the value bytes
101    #[inline]
102    #[must_use]
103    pub const fn new(tag: u32, value: &'a [u8]) -> Self {
104        Self { tag, value }
105    }
106
107    /// Returns the field tag.
108    #[inline]
109    #[must_use]
110    pub const fn tag(&self) -> FieldTag {
111        FieldTag(self.tag)
112    }
113
114    /// Returns the value as a string slice.
115    ///
116    /// # Errors
117    /// Returns `DecodeError::InvalidUtf8` if the value is not valid UTF-8.
118    pub fn as_str(&self) -> Result<&'a str, DecodeError> {
119        std::str::from_utf8(self.value).map_err(DecodeError::from)
120    }
121
122    /// Returns the value as an owned String.
123    ///
124    /// # Errors
125    /// Returns `DecodeError::InvalidUtf8` if the value is not valid UTF-8.
126    pub fn to_string(&self) -> Result<String, DecodeError> {
127        self.as_str().map(String::from)
128    }
129
130    /// Parses the value as the specified type.
131    ///
132    /// # Errors
133    /// Returns `DecodeError::InvalidFieldValue` if parsing fails.
134    pub fn parse<T: FromStr>(&self) -> Result<T, DecodeError> {
135        let s = self.as_str()?;
136        s.parse().map_err(|_| DecodeError::InvalidFieldValue {
137            tag: self.tag,
138            reason: format!("failed to parse '{}' as {}", s, std::any::type_name::<T>()),
139        })
140    }
141
142    /// Returns the value as a u64.
143    ///
144    /// # Errors
145    /// Returns `DecodeError::InvalidFieldValue` if the value is not a valid integer.
146    pub fn as_u64(&self) -> Result<u64, DecodeError> {
147        self.parse()
148    }
149
150    /// Returns the value as an i64.
151    ///
152    /// # Errors
153    /// Returns `DecodeError::InvalidFieldValue` if the value is not a valid integer.
154    pub fn as_i64(&self) -> Result<i64, DecodeError> {
155        self.parse()
156    }
157
158    /// Returns the value as a Decimal.
159    ///
160    /// # Errors
161    /// Returns `DecodeError::InvalidFieldValue` if the value is not a valid decimal.
162    pub fn as_decimal(&self) -> Result<Decimal, DecodeError> {
163        self.parse()
164    }
165
166    /// Returns the value as a bool (FIX uses 'Y'/'N').
167    ///
168    /// # Errors
169    /// Returns `DecodeError::InvalidFieldValue` if the value is not 'Y' or 'N'.
170    pub fn as_bool(&self) -> Result<bool, DecodeError> {
171        match self.value {
172            b"Y" => Ok(true),
173            b"N" => Ok(false),
174            _ => Err(DecodeError::InvalidFieldValue {
175                tag: self.tag,
176                reason: "expected 'Y' or 'N'".to_string(),
177            }),
178        }
179    }
180
181    /// Returns the value as a single character.
182    ///
183    /// # Errors
184    /// Returns `DecodeError::InvalidFieldValue` if the value is not a single ASCII character.
185    pub fn as_char(&self) -> Result<char, DecodeError> {
186        if self.value.len() == 1 && self.value[0].is_ascii() {
187            Ok(self.value[0] as char)
188        } else {
189            Err(DecodeError::InvalidFieldValue {
190                tag: self.tag,
191                reason: "expected single ASCII character".to_string(),
192            })
193        }
194    }
195
196    /// Returns the raw bytes of the value.
197    #[inline]
198    #[must_use]
199    pub const fn as_bytes(&self) -> &'a [u8] {
200        self.value
201    }
202
203    /// Returns the length of the value in bytes.
204    #[inline]
205    #[must_use]
206    pub const fn len(&self) -> usize {
207        self.value.len()
208    }
209
210    /// Returns true if the value is empty.
211    #[inline]
212    #[must_use]
213    pub const fn is_empty(&self) -> bool {
214        self.value.is_empty()
215    }
216}
217
218/// Enumeration of possible FIX field value types.
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220pub enum FieldValue {
221    /// String value.
222    String(String),
223    /// Integer value.
224    Int(i64),
225    /// Unsigned integer value.
226    UInt(u64),
227    /// Decimal/float value.
228    Decimal(Decimal),
229    /// Boolean value (Y/N).
230    Bool(bool),
231    /// Single character value.
232    Char(char),
233    /// Raw bytes (for data fields).
234    Data(Bytes),
235}
236
237impl FieldValue {
238    /// Returns the value as a string, if it is a String variant.
239    #[must_use]
240    pub fn as_str(&self) -> Option<&str> {
241        match self {
242            Self::String(s) => Some(s),
243            _ => None,
244        }
245    }
246
247    /// Returns the value as an i64, if it is an Int variant.
248    #[must_use]
249    pub const fn as_i64(&self) -> Option<i64> {
250        match self {
251            Self::Int(v) => Some(*v),
252            _ => None,
253        }
254    }
255
256    /// Returns the value as a u64, if it is a UInt variant.
257    #[must_use]
258    pub const fn as_u64(&self) -> Option<u64> {
259        match self {
260            Self::UInt(v) => Some(*v),
261            _ => None,
262        }
263    }
264
265    /// Returns the value as a Decimal, if it is a Decimal variant.
266    #[must_use]
267    pub const fn as_decimal(&self) -> Option<Decimal> {
268        match self {
269            Self::Decimal(v) => Some(*v),
270            _ => None,
271        }
272    }
273
274    /// Returns the value as a bool, if it is a Bool variant.
275    #[must_use]
276    pub const fn as_bool(&self) -> Option<bool> {
277        match self {
278            Self::Bool(v) => Some(*v),
279            _ => None,
280        }
281    }
282
283    /// Returns the value as a char, if it is a Char variant.
284    #[must_use]
285    pub const fn as_char(&self) -> Option<char> {
286        match self {
287            Self::Char(v) => Some(*v),
288            _ => None,
289        }
290    }
291}
292
293impl fmt::Display for FieldValue {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        match self {
296            Self::String(s) => write!(f, "{}", s),
297            Self::Int(v) => write!(f, "{}", v),
298            Self::UInt(v) => write!(f, "{}", v),
299            Self::Decimal(v) => write!(f, "{}", v),
300            Self::Bool(v) => write!(f, "{}", if *v { "Y" } else { "N" }),
301            Self::Char(c) => write!(f, "{}", c),
302            Self::Data(d) => write!(f, "<{} bytes>", d.len()),
303        }
304    }
305}
306
307/// Trait for typed FIX field access.
308///
309/// This trait is implemented by generated field types to provide
310/// type-safe access to field values.
311pub trait FixField: Sized {
312    /// The tag number for this field.
313    const TAG: u32;
314
315    /// The Rust type for this field's value.
316    type Value;
317
318    /// Decodes the field value from a byte slice.
319    ///
320    /// # Arguments
321    /// * `bytes` - The raw bytes of the field value
322    ///
323    /// # Errors
324    /// Returns `DecodeError` if the value cannot be decoded.
325    fn decode(bytes: &[u8]) -> Result<Self::Value, DecodeError>;
326
327    /// Encodes the field value to bytes.
328    ///
329    /// # Arguments
330    /// * `value` - The value to encode
331    /// * `buf` - The buffer to write to
332    fn encode(value: &Self::Value, buf: &mut Vec<u8>);
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn test_field_tag() {
341        let tag = FieldTag::new(35);
342        assert_eq!(tag.value(), 35);
343        assert!(tag.is_standard());
344        assert!(!tag.is_user_defined());
345
346        let user_tag = FieldTag::new(5001);
347        assert!(!user_tag.is_standard());
348        assert!(user_tag.is_user_defined());
349    }
350
351    #[test]
352    fn test_field_ref_as_str() {
353        let field = FieldRef::new(11, b"ORDER123");
354        assert_eq!(field.as_str().unwrap(), "ORDER123");
355    }
356
357    #[test]
358    fn test_field_ref_as_u64() {
359        let field = FieldRef::new(34, b"12345");
360        assert_eq!(field.as_u64().unwrap(), 12345);
361    }
362
363    #[test]
364    fn test_field_ref_as_bool() {
365        let yes = FieldRef::new(141, b"Y");
366        let no = FieldRef::new(141, b"N");
367        assert!(yes.as_bool().unwrap());
368        assert!(!no.as_bool().unwrap());
369    }
370
371    #[test]
372    fn test_field_ref_as_char() {
373        let field = FieldRef::new(54, b"1");
374        assert_eq!(field.as_char().unwrap(), '1');
375    }
376
377    #[test]
378    fn test_field_ref_invalid_utf8() {
379        let field = FieldRef::new(1, &[0xFF, 0xFE]);
380        assert!(field.as_str().is_err());
381    }
382
383    #[test]
384    fn test_field_value_display() {
385        assert_eq!(FieldValue::String("test".to_string()).to_string(), "test");
386        assert_eq!(FieldValue::Int(42).to_string(), "42");
387        assert_eq!(FieldValue::Bool(true).to_string(), "Y");
388        assert_eq!(FieldValue::Bool(false).to_string(), "N");
389    }
390}