Skip to main content

truefix_core/
message.rs

1//! A FIX message: header, body, and trailer field maps.
2
3use crate::error::DecodeError;
4use crate::field_map::FieldMap;
5use crate::tags::{BEGIN_STRING, MSG_TYPE};
6
7/// A FIX message split into header, body, and trailer regions.
8///
9/// Decoding routes session-level header tags and trailer tags to their regions; everything
10/// else lands in the body. Encoding always emits the canonical order
11/// `8, 9, 35, <rest of header>, <body>, <trailer except 10>, 10`.
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct Message {
14    /// Header fields (BeginString, BodyLength, MsgType, session fields, ...).
15    pub header: FieldMap,
16    /// Application/body fields.
17    pub body: FieldMap,
18    /// Trailer fields (SignatureLength, Signature, CheckSum).
19    pub trailer: FieldMap,
20    /// Set by [`Self::decode`] when the wire byte stream violated header/body/trailer sectioning
21    /// (a field classified into an earlier section arrived after a later-section field had
22    /// already been seen) or the third field wasn't MsgType(35) — i.e. `ValidateFieldsOutOfOrder`
23    /// (FR-006). Always `false` for a `Message` built in code (assumed well-formed); the 3
24    /// separate `FieldMap`s preserve *within-section* wire order but classify by static tag
25    /// identity, so cross-section interleaving is only observable during decode itself.
26    pub(crate) fields_out_of_order: bool,
27}
28
29impl Message {
30    /// Create an empty message.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// The BeginString (tag 8) as a string, if present and valid UTF-8.
36    pub fn begin_string(&self) -> Option<&str> {
37        self.header.get(BEGIN_STRING).and_then(|f| f.as_str().ok())
38    }
39
40    /// Whether decoding observed header/body/trailer fields out of their wire-sectioning order,
41    /// or a third field other than MsgType(35) (`ValidateFieldsOutOfOrder`; FR-006). Always
42    /// `false` for a `Message` built in code rather than decoded from the wire.
43    pub fn fields_out_of_order(&self) -> bool {
44        self.fields_out_of_order
45    }
46
47    /// The MsgType (tag 35) as a string, if present and valid UTF-8.
48    pub fn msg_type(&self) -> Option<&str> {
49        self.header.get(MSG_TYPE).and_then(|f| f.as_str().ok())
50    }
51
52    /// Encode to wire bytes (computes BodyLength and CheckSum).
53    pub fn encode(&self) -> Vec<u8> {
54        crate::codec::encode(self)
55    }
56
57    /// Decode from wire bytes (verifies BodyLength and CheckSum).
58    pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
59        crate::codec::decode(bytes)
60    }
61
62    /// Copy `original`'s routing header fields onto `self` (a reply/reject being built),
63    /// reversed: `original`'s `OnBehalfOfCompID(115)` becomes `self`'s `DeliverToCompID(128)` and
64    /// vice versa; likewise `OnBehalfOfSubID(116)`/`DeliverToSubID(129)` and
65    /// `OnBehalfOfLocationID(144)`/`DeliverToLocationID(145)` (Appendix B `ReverseRoute`).
66    ///
67    /// A routing tag absent on `original` is left unset on `self` — no reversal is attempted and
68    /// no error is raised (`ReverseRouteWithEmptyRoutingTags`: a tag present with an empty value
69    /// still reverses, since presence — not content — governs).
70    pub fn reverse_route(&mut self, original: &Message) {
71        for (on_behalf_of, deliver_to) in [(115u32, 128u32), (116, 129), (144, 145)] {
72            if let Some(f) = original.header.get(on_behalf_of) {
73                self.header.set(crate::field::Field::new(
74                    deliver_to,
75                    f.value_bytes().to_vec(),
76                ));
77            }
78            if let Some(f) = original.header.get(deliver_to) {
79                self.header.set(crate::field::Field::new(
80                    on_behalf_of,
81                    f.value_bytes().to_vec(),
82                ));
83            }
84        }
85    }
86}