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}