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, EncodeError, InvalidFieldTag};
16use bytes::Bytes;
17use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use std::str::FromStr;
21
22/// The lowest tag number in the original FIX bilateral user-defined range.
23///
24/// FIX reserves 5000-9999 for bilateral user-defined fields, so 4999 is the
25/// highest standard tag below the range and 5000 the first user-defined one.
26pub const USER_DEFINED_TAG_MIN: u32 = 5000;
27
28/// The highest tag number in the original FIX bilateral user-defined range.
29///
30/// The first user-defined range is 5000-9999. Tags 10000-19999 are reserved
31/// for internal use within a single firm and are not the bilateral range.
32pub const USER_DEFINED_TAG_MAX: u32 = 9999;
33
34/// The lowest tag number in the extended FIX bilateral user-defined range.
35///
36/// The Global Technical Committee approved 20000-39999 as a second bilateral
37/// user-defined range in December 2009, once the 5000-9999 range filled up;
38/// tags here are used bilaterally and do not need to be registered.
39pub const USER_DEFINED_EXT_TAG_MIN: u32 = 20000;
40
41/// The highest tag number in the extended FIX bilateral user-defined range.
42///
43/// The extended user-defined range is 20000-39999. Tags at or above 40000 are
44/// reserved (GTC / internal use), not user-defined; the loaded dictionary, not
45/// this type, is what says what an individual high tag means.
46pub const USER_DEFINED_EXT_TAG_MAX: u32 = 39999;
47
48/// FIX field tag number.
49///
50/// Tags are positive integers that identify fields within a FIX message.
51/// Standard tags are defined in the FIX specification (1-4999). FIX reserves
52/// **two** disjoint bilateral user-defined ranges: 5000-9999, and 20000-39999
53/// (approved by the GTC in December 2009 once the first filled up). Tags
54/// 10000-19999 are internal-use, and tags at or above 40000 are reserved, so
55/// neither is user-defined. Only the two user-defined ranges classify as such
56/// here; a dictionary — not this type — is what says whether a given tag is
57/// known.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
59#[repr(transparent)]
60#[serde(transparent)]
61pub struct FieldTag(u32);
62
63impl FieldTag {
64    /// Creates a new field tag **without validating it**.
65    ///
66    /// `tag` is not checked against [`FieldTag::is_valid`]; `FieldTag::new(0)`
67    /// yields a tag that is neither standard nor user-defined. Use
68    /// [`FieldTag::try_new`] for a value that came off the wire or from a
69    /// caller.
70    #[inline]
71    #[must_use]
72    pub const fn new(tag: u32) -> Self {
73        Self(tag)
74    }
75
76    /// Creates a new field tag, rejecting numbers that are not legal FIX tags.
77    ///
78    /// # Errors
79    /// Returns [`InvalidFieldTag`] if `tag` is `0`.
80    #[inline]
81    pub const fn try_new(tag: u32) -> Result<Self, InvalidFieldTag> {
82        if tag == 0 {
83            return Err(InvalidFieldTag::new(tag));
84        }
85        Ok(Self(tag))
86    }
87
88    /// Returns the raw tag number.
89    #[inline]
90    #[must_use]
91    pub const fn value(self) -> u32 {
92        self.0
93    }
94
95    /// Returns true if this is a legal FIX tag number (>= 1).
96    #[inline]
97    #[must_use]
98    pub const fn is_valid(self) -> bool {
99        self.0 >= 1
100    }
101
102    /// Returns true if this is a standard FIX tag below the user-defined range
103    /// (1-4999).
104    ///
105    /// This does not cover assigned tags above 9999: the dictionary — not this
106    /// predicate — classifies an individual high tag.
107    #[inline]
108    #[must_use]
109    pub const fn is_standard(self) -> bool {
110        self.0 >= 1 && self.0 < USER_DEFINED_TAG_MIN
111    }
112
113    /// Returns true if this is a bilateral user-defined tag.
114    ///
115    /// FIX defines **two** disjoint user-defined ranges:
116    /// [`USER_DEFINED_TAG_MIN`]..=[`USER_DEFINED_TAG_MAX`] (5000-9999) and
117    /// [`USER_DEFINED_EXT_TAG_MIN`]..=[`USER_DEFINED_EXT_TAG_MAX`]
118    /// (20000-39999). A tag in the 10000-19999 internal-use range, or at or
119    /// above 40000, is deliberately *not* user-defined.
120    #[inline]
121    #[must_use]
122    pub const fn is_user_defined(self) -> bool {
123        (self.0 >= USER_DEFINED_TAG_MIN && self.0 <= USER_DEFINED_TAG_MAX)
124            || (self.0 >= USER_DEFINED_EXT_TAG_MIN && self.0 <= USER_DEFINED_EXT_TAG_MAX)
125    }
126}
127
128impl From<u32> for FieldTag {
129    fn from(tag: u32) -> Self {
130        Self(tag)
131    }
132}
133
134impl From<FieldTag> for u32 {
135    fn from(tag: FieldTag) -> Self {
136        tag.0
137    }
138}
139
140impl fmt::Display for FieldTag {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "{}", self.0)
143    }
144}
145
146/// Zero-copy reference to a field within a FIX message buffer.
147///
148/// This struct holds references to the original message buffer,
149/// avoiding allocation during parsing.
150#[derive(Debug, Clone, Copy)]
151pub struct FieldRef<'a> {
152    /// The field tag number.
153    pub tag: u32,
154    /// Reference to the field value bytes (without delimiters).
155    pub value: &'a [u8],
156}
157
158impl<'a> FieldRef<'a> {
159    /// Creates a new field reference.
160    ///
161    /// # Arguments
162    /// * `tag` - The field tag number
163    /// * `value` - Reference to the value bytes
164    #[inline]
165    #[must_use]
166    pub const fn new(tag: u32, value: &'a [u8]) -> Self {
167        Self { tag, value }
168    }
169
170    /// Returns the field tag.
171    #[inline]
172    #[must_use]
173    pub const fn tag(&self) -> FieldTag {
174        FieldTag(self.tag)
175    }
176
177    /// Returns the value as a string slice.
178    ///
179    /// # Errors
180    /// Returns `DecodeError::InvalidUtf8` if the value is not valid UTF-8.
181    pub fn as_str(&self) -> Result<&'a str, DecodeError> {
182        std::str::from_utf8(self.value).map_err(DecodeError::from)
183    }
184
185    /// Returns the value as an owned String.
186    ///
187    /// # Errors
188    /// Returns `DecodeError::InvalidUtf8` if the value is not valid UTF-8.
189    pub fn to_string(&self) -> Result<String, DecodeError> {
190        self.as_str().map(String::from)
191    }
192
193    /// Parses the value as the specified type.
194    ///
195    /// # Errors
196    /// Returns `DecodeError::InvalidFieldValue` if parsing fails.
197    pub fn parse<T: FromStr>(&self) -> Result<T, DecodeError> {
198        let s = self.as_str()?;
199        s.parse().map_err(|_| DecodeError::InvalidFieldValue {
200            tag: self.tag,
201            reason: format!("failed to parse '{}' as {}", s, std::any::type_name::<T>()),
202        })
203    }
204
205    /// Returns the value as a u64.
206    ///
207    /// # Errors
208    /// Returns `DecodeError::InvalidFieldValue` if the value is not a valid integer.
209    pub fn as_u64(&self) -> Result<u64, DecodeError> {
210        self.parse()
211    }
212
213    /// Returns the value as an i64.
214    ///
215    /// # Errors
216    /// Returns `DecodeError::InvalidFieldValue` if the value is not a valid integer.
217    pub fn as_i64(&self) -> Result<i64, DecodeError> {
218        self.parse()
219    }
220
221    /// Returns the value as a Decimal.
222    ///
223    /// # Errors
224    /// Returns `DecodeError::InvalidFieldValue` if the value is not a valid decimal.
225    pub fn as_decimal(&self) -> Result<Decimal, DecodeError> {
226        self.parse()
227    }
228
229    /// Returns the value as a bool (FIX uses 'Y'/'N').
230    ///
231    /// # Errors
232    /// Returns `DecodeError::InvalidFieldValue` if the value is not 'Y' or 'N'.
233    pub fn as_bool(&self) -> Result<bool, DecodeError> {
234        match self.value {
235            b"Y" => Ok(true),
236            b"N" => Ok(false),
237            _ => Err(DecodeError::InvalidFieldValue {
238                tag: self.tag,
239                reason: "expected 'Y' or 'N'".to_string(),
240            }),
241        }
242    }
243
244    /// Returns the value as a single character.
245    ///
246    /// # Errors
247    /// Returns `DecodeError::InvalidFieldValue` if the value is not a single ASCII character.
248    pub fn as_char(&self) -> Result<char, DecodeError> {
249        if self.value.len() == 1 && self.value[0].is_ascii() {
250            Ok(self.value[0] as char)
251        } else {
252            Err(DecodeError::InvalidFieldValue {
253                tag: self.tag,
254                reason: "expected single ASCII character".to_string(),
255            })
256        }
257    }
258
259    /// Returns the raw bytes of the value.
260    #[inline]
261    #[must_use]
262    pub const fn as_bytes(&self) -> &'a [u8] {
263        self.value
264    }
265
266    /// Returns the length of the value in bytes.
267    #[inline]
268    #[must_use]
269    pub const fn len(&self) -> usize {
270        self.value.len()
271    }
272
273    /// Returns true if the value is empty.
274    #[inline]
275    #[must_use]
276    pub const fn is_empty(&self) -> bool {
277        self.value.is_empty()
278    }
279}
280
281/// Enumeration of possible FIX field value types.
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub enum FieldValue {
284    /// String value.
285    String(String),
286    /// Integer value.
287    Int(i64),
288    /// Unsigned integer value.
289    UInt(u64),
290    /// Decimal/float value.
291    Decimal(Decimal),
292    /// Boolean value (Y/N).
293    Bool(bool),
294    /// Single character value.
295    Char(char),
296    /// Raw bytes (for data fields).
297    Data(Bytes),
298}
299
300impl FieldValue {
301    /// Returns the value as a string, if it is a String variant.
302    #[must_use]
303    pub fn as_str(&self) -> Option<&str> {
304        match self {
305            Self::String(s) => Some(s),
306            _ => None,
307        }
308    }
309
310    /// Returns the value as an i64, if it is an Int variant.
311    #[must_use]
312    pub const fn as_i64(&self) -> Option<i64> {
313        match self {
314            Self::Int(v) => Some(*v),
315            _ => None,
316        }
317    }
318
319    /// Returns the value as a u64, if it is a UInt variant.
320    #[must_use]
321    pub const fn as_u64(&self) -> Option<u64> {
322        match self {
323            Self::UInt(v) => Some(*v),
324            _ => None,
325        }
326    }
327
328    /// Returns the value as a Decimal, if it is a Decimal variant.
329    #[must_use]
330    pub const fn as_decimal(&self) -> Option<Decimal> {
331        match self {
332            Self::Decimal(v) => Some(*v),
333            _ => None,
334        }
335    }
336
337    /// Returns the value as a bool, if it is a Bool variant.
338    #[must_use]
339    pub const fn as_bool(&self) -> Option<bool> {
340        match self {
341            Self::Bool(v) => Some(*v),
342            _ => None,
343        }
344    }
345
346    /// Returns the value as a char, if it is a Char variant.
347    #[must_use]
348    pub const fn as_char(&self) -> Option<char> {
349        match self {
350            Self::Char(v) => Some(*v),
351            _ => None,
352        }
353    }
354}
355
356impl fmt::Display for FieldValue {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        match self {
359            Self::String(s) => write!(f, "{}", s),
360            Self::Int(v) => write!(f, "{}", v),
361            Self::UInt(v) => write!(f, "{}", v),
362            Self::Decimal(v) => write!(f, "{}", v),
363            Self::Bool(v) => write!(f, "{}", if *v { "Y" } else { "N" }),
364            Self::Char(c) => write!(f, "{}", c),
365            Self::Data(d) => write!(f, "<{} bytes>", d.len()),
366        }
367    }
368}
369
370/// Trait for typed FIX field access.
371///
372/// This trait is implemented by generated field types to provide
373/// type-safe access to field values.
374pub trait FixField: Sized {
375    /// The tag number for this field.
376    const TAG: u32;
377
378    /// The Rust type for this field's value.
379    type Value;
380
381    /// Decodes the field value from a byte slice.
382    ///
383    /// # Arguments
384    /// * `bytes` - The raw bytes of the field value
385    ///
386    /// # Errors
387    /// Returns `DecodeError` if the value cannot be decoded.
388    fn decode(bytes: &[u8]) -> Result<Self::Value, DecodeError>;
389
390    /// Encodes the field value to bytes.
391    ///
392    /// # Arguments
393    /// * `value` - The value to encode
394    /// * `buf` - The buffer to write to
395    ///
396    /// # Errors
397    /// Returns [`EncodeError`] if `value` has no legal on-the-wire form — for
398    /// example a string carrying the SOH delimiter
399    /// ([`EncodeError::InvalidFieldValue`]) or one past a length bound
400    /// ([`EncodeError::FieldTooLong`]). Mirrors
401    /// [`crate::message::FixMessage::encode`], so an implementor never has to
402    /// panic or emit corrupt bytes.
403    fn encode(value: &Self::Value, buf: &mut Vec<u8>) -> Result<(), EncodeError>;
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    /// Unwraps a `Result` with test context instead of `.unwrap()`.
411    #[track_caller]
412    fn ok<T, E: fmt::Debug>(result: Result<T, E>, what: &str) -> T {
413        match result {
414            Ok(value) => value,
415            Err(err) => panic!("{what}: {err:?}"),
416        }
417    }
418
419    /// Asserts that `result` failed with `InvalidFieldValue` for `tag`.
420    #[track_caller]
421    fn assert_invalid_field_value<T: fmt::Debug>(result: Result<T, DecodeError>, tag: u32) {
422        match result {
423            Err(DecodeError::InvalidFieldValue { tag: actual, .. }) => assert_eq!(actual, tag),
424            other => panic!("expected InvalidFieldValue for tag {tag}, got {other:?}"),
425        }
426    }
427
428    #[test]
429    fn test_field_tag_standard_range() {
430        let tag = FieldTag::new(35);
431        assert_eq!(tag.value(), 35);
432        assert!(tag.is_valid());
433        assert!(tag.is_standard());
434        assert!(!tag.is_user_defined());
435    }
436
437    #[test]
438    fn test_field_tag_zero_is_neither_standard_nor_user_defined() {
439        let zero = FieldTag::new(0);
440        assert!(!zero.is_valid());
441        assert!(!zero.is_standard());
442        assert!(!zero.is_user_defined());
443        assert_eq!(FieldTag::try_new(0), Err(InvalidFieldTag::new(0)));
444    }
445
446    #[test]
447    fn test_field_tag_boundary_4999_is_standard() {
448        let tag = ok(FieldTag::try_new(4999), "4999 is a legal tag");
449        assert!(tag.is_standard());
450        assert!(!tag.is_user_defined());
451    }
452
453    #[test]
454    fn test_field_tag_boundary_5000_is_user_defined() {
455        let tag = ok(FieldTag::try_new(5000), "5000 is a legal tag");
456        assert!(!tag.is_standard());
457        assert!(tag.is_user_defined());
458        assert_eq!(tag.value(), USER_DEFINED_TAG_MIN);
459    }
460
461    #[test]
462    fn test_field_tag_boundary_9999_is_user_defined() {
463        let tag = ok(FieldTag::try_new(9999), "9999 is a legal tag");
464        assert!(!tag.is_standard());
465        assert!(tag.is_user_defined());
466        assert_eq!(tag.value(), USER_DEFINED_TAG_MAX);
467    }
468
469    #[test]
470    fn test_field_tag_boundary_10000_is_not_user_defined() {
471        // 10000-19999 is internal-use, between the two bilateral ranges.
472        let tag = ok(FieldTag::try_new(10000), "10000 is a legal tag");
473        assert!(!tag.is_user_defined());
474        assert!(!tag.is_standard());
475    }
476
477    #[test]
478    fn test_field_tag_extended_user_defined_range_boundaries() {
479        // The GTC approved 20000-39999 as a second bilateral user-defined range
480        // in 2009; the earlier `<= 9999` bound regressed all of it to false.
481        // 19999 is still internal-use; 40000+ is reserved.
482        let cases = [
483            (19999u32, false),
484            (20000, true),
485            (USER_DEFINED_EXT_TAG_MIN, true),
486            (30000, true),
487            (39999, true),
488            (USER_DEFINED_EXT_TAG_MAX, true),
489            (40000, false),
490        ];
491        for (tag_num, expected) in cases {
492            let tag = ok(FieldTag::try_new(tag_num), "high tag is legal");
493            assert_eq!(
494                tag.is_user_defined(),
495                expected,
496                "is_user_defined({tag_num}) should be {expected}"
497            );
498            assert!(!tag.is_standard(), "{tag_num} is not a standard low tag");
499        }
500    }
501
502    #[test]
503    fn test_field_tag_reserved_high_range_is_not_user_defined() {
504        // Tags at or above 40000 are reserved (GTC / internal use), not
505        // user-defined; the original `>= 5000` predicate misclassified them.
506        for tag_num in [40000, 40001, 49999, 50000] {
507            let tag = ok(FieldTag::try_new(tag_num), "reserved high tag is legal");
508            assert!(
509                !tag.is_user_defined(),
510                "reserved tag {tag_num} must not be user-defined"
511            );
512        }
513    }
514
515    #[test]
516    fn test_field_ref_as_str() {
517        let field = FieldRef::new(11, b"ORDER123");
518        assert_eq!(field.as_str(), Ok("ORDER123"));
519    }
520
521    #[test]
522    fn test_field_ref_as_u64() {
523        let field = FieldRef::new(34, b"12345");
524        assert_eq!(field.as_u64(), Ok(12345));
525    }
526
527    #[test]
528    fn test_field_ref_as_u64_non_numeric_is_typed_error() {
529        assert_invalid_field_value(FieldRef::new(34, b"abc").as_u64(), 34);
530    }
531
532    #[test]
533    fn test_field_ref_as_u64_negative_is_typed_error() {
534        assert_invalid_field_value(FieldRef::new(34, b"-1").as_u64(), 34);
535    }
536
537    #[test]
538    fn test_field_ref_as_u64_empty_is_typed_error() {
539        assert_invalid_field_value(FieldRef::new(34, b"").as_u64(), 34);
540    }
541
542    #[test]
543    fn test_field_ref_as_i64_accepts_negative() {
544        assert_eq!(FieldRef::new(14, b"-42").as_i64(), Ok(-42));
545    }
546
547    #[test]
548    fn test_field_ref_as_decimal_parses_price() {
549        let field = FieldRef::new(44, b"123.45");
550        let price = ok(field.as_decimal(), "123.45 is a valid price");
551        assert_eq!(price, Decimal::new(12345, 2));
552    }
553
554    #[test]
555    fn test_field_ref_as_decimal_preserves_scale() {
556        // Trailing zeros are significant on the wire; the parse keeps them.
557        let field = FieldRef::new(44, b"1.500");
558        let price = ok(field.as_decimal(), "1.500 is a valid price");
559        assert_eq!(price.to_string(), "1.500");
560    }
561
562    #[test]
563    fn test_field_ref_as_decimal_negative_price() {
564        let field = FieldRef::new(44, b"-0.01");
565        let price = ok(field.as_decimal(), "-0.01 is a valid price");
566        assert_eq!(price, Decimal::new(-1, 2));
567    }
568
569    #[test]
570    fn test_field_ref_as_decimal_two_points_is_typed_error() {
571        assert_invalid_field_value(FieldRef::new(44, b"1.2.3").as_decimal(), 44);
572    }
573
574    #[test]
575    fn test_field_ref_as_decimal_garbage_is_typed_error() {
576        assert_invalid_field_value(FieldRef::new(44, b"abc").as_decimal(), 44);
577        assert_invalid_field_value(FieldRef::new(44, b"").as_decimal(), 44);
578        assert_invalid_field_value(FieldRef::new(44, b"1,50").as_decimal(), 44);
579    }
580
581    #[test]
582    fn test_field_ref_as_decimal_invalid_utf8_is_typed_error() {
583        let field = FieldRef::new(44, &[0xFF, 0xFE]);
584        assert!(matches!(
585            field.as_decimal(),
586            Err(DecodeError::InvalidUtf8(_))
587        ));
588    }
589
590    #[test]
591    fn test_field_ref_as_bool() {
592        assert_eq!(FieldRef::new(141, b"Y").as_bool(), Ok(true));
593        assert_eq!(FieldRef::new(141, b"N").as_bool(), Ok(false));
594    }
595
596    #[test]
597    fn test_field_ref_as_bool_rejects_lowercase_and_words() {
598        assert_invalid_field_value(FieldRef::new(141, b"y").as_bool(), 141);
599        assert_invalid_field_value(FieldRef::new(141, b"n").as_bool(), 141);
600        assert_invalid_field_value(FieldRef::new(141, b"YES").as_bool(), 141);
601        assert_invalid_field_value(FieldRef::new(141, b"").as_bool(), 141);
602    }
603
604    #[test]
605    fn test_field_ref_as_char() {
606        assert_eq!(FieldRef::new(54, b"1").as_char(), Ok('1'));
607    }
608
609    #[test]
610    fn test_field_ref_as_char_rejects_multi_byte_and_non_ascii() {
611        assert_invalid_field_value(FieldRef::new(54, b"12").as_char(), 54);
612        assert_invalid_field_value(FieldRef::new(54, b"").as_char(), 54);
613        // 'ñ' is two bytes and neither is ASCII.
614        assert_invalid_field_value(FieldRef::new(54, "ñ".as_bytes()).as_char(), 54);
615        assert_invalid_field_value(FieldRef::new(54, &[0x80]).as_char(), 54);
616    }
617
618    #[test]
619    fn test_field_ref_invalid_utf8() {
620        let field = FieldRef::new(1, &[0xFF, 0xFE]);
621        assert!(matches!(field.as_str(), Err(DecodeError::InvalidUtf8(_))));
622    }
623
624    #[test]
625    fn test_field_value_display() {
626        assert_eq!(FieldValue::String("test".to_string()).to_string(), "test");
627        assert_eq!(FieldValue::Int(42).to_string(), "42");
628        assert_eq!(FieldValue::Bool(true).to_string(), "Y");
629        assert_eq!(FieldValue::Bool(false).to_string(), "N");
630    }
631}