Skip to main content

fixlite/
builder.rs

1use crate::fix::{DayOfMonth, FixedPrice};
2use chrono::{DateTime, Datelike, Timelike, Utc};
3
4/// FIX field delimiter (SOH / 0x01).
5pub const SOH: u8 = 0x01;
6/// Maximum decimal digits required to encode a `usize` body length.
7/// `log10(usize::MAX) + 1 = 20` on 64-bit; an over-estimate on 32-bit is harmless.
8const MAX_BODY_LEN_DIGITS: usize = 20;
9
10/// Build a FIX message with a single macro invocation.
11///
12/// This expands to `begin_with(...).field(...).finish()` using `FixBuilder`.
13/// Supports:
14/// - `tag => value` for infallible fields
15/// - `?tag => value` for fallible fields (currently `f64`); expands to
16///   `try_field_ref(...)?` and requires a `Result`-returning context
17/// - `@value` for tagged values whose tag is provided by the type
18/// - legacy `tag, value` pairs (equivalent to `tag => value`)
19#[macro_export]
20macro_rules! build_fix {
21    ($builder:expr, $seq_out:expr, $dt:expr, $msg_type:expr $(,)?) => {{
22        $builder
23            .begin_with(&$seq_out, &$dt, &$msg_type)
24            .finish()
25    }};
26    ($builder:expr, $seq_out:expr, $dt:expr, $msg_type:expr $(, $($rest:tt)+)?) => {{
27        let msg = $builder.begin_with(&$seq_out, &$dt, &$msg_type);
28        $crate::build_fix!(@fields msg $(, $($rest)+)?).finish()
29    }};
30    (@fields $msg:expr) => { $msg };
31    (@fields $msg:expr, ) => { $msg };
32    (@fields $msg:expr, ? $tag:expr => $val:expr ,) => {
33        $msg.try_field_ref($tag as u32, &$val)?
34    };
35    (@fields $msg:expr, $tag:expr => $val:expr ,) => {
36        $msg.field_ref($tag as u32, &$val)
37    };
38    (@fields $msg:expr, @$val:expr ,) => {
39        $msg.field_tagged_ref(&$val)
40    };
41    (@fields $msg:expr, $tag:expr, $val:expr ,) => {
42        $msg.field_ref($tag as u32, &$val)
43    };
44    (@fields $msg:expr, ? $tag:expr => $val:expr $(, $($rest:tt)+)?) => {
45        $crate::build_fix!(@fields $msg.try_field_ref($tag as u32, &$val)? $(, $($rest)+)?)
46    };
47    (@fields $msg:expr, $tag:expr => $val:expr $(, $($rest:tt)+)?) => {
48        $crate::build_fix!(@fields $msg.field_ref($tag as u32, &$val) $(, $($rest)+)?)
49    };
50    (@fields $msg:expr, @$val:expr $(, $($rest:tt)+)?) => {
51        $crate::build_fix!(@fields $msg.field_tagged_ref(&$val) $(, $($rest)+)?)
52    };
53    (@fields $msg:expr, $tag:expr, $val:expr $(, $($rest:tt)+)?) => {
54        $crate::build_fix!(@fields $msg.field_ref($tag as u32, &$val) $(, $($rest)+)?)
55    };
56}
57
58/// Values that can be encoded as a FIX field value (no heap allocation required).
59pub trait FixValue {
60    /// Encode the value into `out` without allocating.
61    fn encode(&self, out: &mut Vec<u8>);
62}
63
64/// Values that map to a single, fixed FIX tag.
65pub trait FixTaggedValue: FixValue {
66    /// The FIX tag associated with this type.
67    const TAG: u32;
68}
69
70/// Fallible FIX value encoding for builders that need validation.
71///
72/// Note: in this crate, only `f64` is fallible (NaN/inf); other impls are infallible.
73pub trait TryFixValue {
74    /// Error returned when encoding fails.
75    type Error;
76    /// Encode the value into `out`, returning an error if the value is invalid.
77    fn try_encode(&self, out: &mut Vec<u8>) -> Result<(), Self::Error>;
78}
79
80impl TryFixValue for f64 {
81    type Error = FixError;
82
83    /// Encode an `f64` into a FIX field value.
84    ///
85    /// Wire contract: 15 significant decimal digits, banker's rounding
86    /// (round-half-to-even), no exponential notation, trailing fractional
87    /// zeros trimmed. `NaN` and infinities return `FixError::InvalidValue`.
88    ///
89    /// This is *not* shortest-round-trippable: parsing the emitted bytes
90    /// back as `f64` may yield a value differing in the last few low bits.
91    /// For values where the last bit matters, prefer `FixedPrice<W, F>`.
92    /// See `fixlite_example/benches/f64_encode_bench.rs` for the perf
93    /// motivation behind the custom path.
94    #[inline]
95    fn try_encode(&self, out: &mut Vec<u8>) -> Result<(), Self::Error> {
96        if encode_f64_checked(*self, out) {
97            Ok(())
98        } else {
99            // tag=0 placeholder; try_kv() patches it to the actual tag
100            Err(FixError::invalid_value(0))
101        }
102    }
103}
104
105/// Marker trait: values suitable for FIX tag 34 (MsgSeqNum).
106pub trait FixSeqNum: FixValue {}
107
108// --- Common FixSeqNum impls ---
109impl FixSeqNum for u32 {}
110impl FixSeqNum for u64 {}
111impl FixSeqNum for usize {}
112impl FixSeqNum for i64 {}
113
114/// Marker trait: values suitable for FIX tag 52 (SendingTime, UTCTimestamp format).
115pub trait FixSendingTime {
116    /// Encode the sending time into `out` (YYYYMMDD-HH:MM:SS.mmm).
117    fn encode_sending_time(&self, out: &mut Vec<u8>);
118}
119
120// --- Common FixSendingTime impls ---
121impl FixSendingTime for DateTime<Utc> {
122    #[inline]
123    fn encode_sending_time(&self, out: &mut Vec<u8>) {
124        encode_timestamp_utc(self, out);
125    }
126}
127
128/// FixSendingTime wrapper for a preformatted str.
129pub struct SendingTimeStr<'a>(pub &'a str);
130
131impl FixSendingTime for SendingTimeStr<'_> {
132    #[inline]
133    fn encode_sending_time(&self, out: &mut Vec<u8>) {
134        out.extend_from_slice(self.0.as_bytes());
135    }
136}
137
138/// FixSendingTime wrapper for preformatted bytes.
139pub struct SendingTimeBytes<'a>(pub &'a [u8]);
140
141impl FixSendingTime for SendingTimeBytes<'_> {
142    #[inline]
143    fn encode_sending_time(&self, out: &mut Vec<u8>) {
144        out.extend_from_slice(self.0);
145    }
146}
147
148/// Convenience trait for enums that map to a static FIX code ("D", "1", "2", ...).
149pub trait AsFixStr {
150    /// Return the static FIX code for this value.
151    fn as_fix_str(&self) -> &'static str;
152}
153
154impl<T: AsFixStr> FixValue for T {
155    #[inline]
156    fn encode(&self, out: &mut Vec<u8>) {
157        out.extend_from_slice(self.as_fix_str().as_bytes());
158    }
159}
160
161// --- Common FixValue impls ---
162
163impl FixValue for str {
164    #[inline]
165    fn encode(&self, out: &mut Vec<u8>) {
166        out.extend_from_slice(self.as_bytes());
167    }
168}
169
170impl FixValue for String {
171    #[inline]
172    fn encode(&self, out: &mut Vec<u8>) {
173        out.extend_from_slice(self.as_bytes());
174    }
175}
176
177impl FixValue for [u8] {
178    #[inline]
179    fn encode(&self, out: &mut Vec<u8>) {
180        out.extend_from_slice(self);
181    }
182}
183
184impl FixValue for bool {
185    #[inline]
186    fn encode(&self, out: &mut Vec<u8>) {
187        out.push(if *self { b'Y' } else { b'N' });
188    }
189}
190
191impl FixValue for u8 {
192    #[inline]
193    fn encode(&self, out: &mut Vec<u8>) {
194        push_u64_ascii(out, *self as u64);
195    }
196}
197
198impl FixValue for u32 {
199    #[inline]
200    fn encode(&self, out: &mut Vec<u8>) {
201        push_u64_ascii(out, *self as u64);
202    }
203}
204impl FixValue for u64 {
205    #[inline]
206    fn encode(&self, out: &mut Vec<u8>) {
207        push_u64_ascii(out, *self);
208    }
209}
210impl FixValue for usize {
211    #[inline]
212    fn encode(&self, out: &mut Vec<u8>) {
213        push_u64_ascii(out, *self as u64);
214    }
215}
216impl FixValue for i64 {
217    #[inline]
218    fn encode(&self, out: &mut Vec<u8>) {
219        push_i64_ascii(out, *self);
220    }
221}
222impl FixValue for i32 {
223    #[inline]
224    fn encode(&self, out: &mut Vec<u8>) {
225        (*self as i64).encode(out)
226    }
227}
228
229const DIGITS_U16: [u16; 100] = digits_00_99_u16();
230
231// `f64` intentionally does NOT implement `FixValue`. Encoding can fail for
232// NaN/inf, and a silent infallible path would emit a malformed FIX field
233// (`tag=<SOH>`). Use `try_field` / `try_field_ref` / the `?tag => val` macro
234// arm to encode an `f64`, or use `FixedPrice<W, F>` for prices where finiteness
235// is statically guaranteed.
236
237#[inline]
238fn encode_f64_checked(mut value: f64, out: &mut Vec<u8>) -> bool {
239    let start = out.len();
240    if !value.is_finite() {
241        return false;
242    }
243    if value < 0.0 {
244        out.push(b'-');
245        value = -value;
246    }
247    let whole = value as u64;
248    let len = out.len();
249    push_u64_ascii(out, whole);
250    let wd = if whole != 0 {
251        (out.len() - len) as u32
252    } else {
253        0
254    };
255
256    if wd < 15 {
257        let mut factor = 10u64.pow(15 - wd);
258        let fraction = value - whole as f64;
259
260        // IEEE-754: round to nearest, ties to even (banker's rounding)
261        let scaled = fraction * (factor as f64);
262        let mut fraction = scaled.round();
263        if fraction <= 0.0 {
264            fraction = 0.0;
265        }
266        let mut fraction = fraction as u64;
267
268        if fraction == factor {
269            if let Some(next) = whole.checked_add(1) {
270                out.truncate(len); // rewind to before writing whole
271                push_u64_ascii(out, next);
272            }
273            // else: whole == u64::MAX, cannot carry; keep already-written whole
274            return true;
275        }
276
277        if fraction > 0 {
278            out.push(b'.');
279            while fraction > 0 {
280                let n: usize; // 0..100 (index into DIGITS_U16)
281
282                if factor >= 100 {
283                    factor /= 100;
284                    n = (fraction / factor) as usize;
285                    fraction %= factor;
286                } else if factor == 10 {
287                    n = (fraction as usize) * 10;
288                    fraction = 0;
289                } else {
290                    n = fraction as usize;
291                    fraction = 0;
292                }
293                let pair = DIGITS_U16[n];
294                let [tens, ones] = pair.to_ne_bytes();
295                if fraction > 0 || ones != b'0' {
296                    push_2_u16(out, pair);
297                } else {
298                    out.push(tens);
299                }
300            }
301        }
302    }
303
304    // written if we appended anything at all
305    out.len() != start
306}
307
308/// FIX timestamp format: YYYYMMDD-HH:MM:SS.mmm
309impl FixValue for DateTime<Utc> {
310    #[inline]
311    fn encode(&self, out: &mut Vec<u8>) {
312        encode_timestamp_utc(self, out);
313    }
314}
315
316impl FixValue for DayOfMonth {
317    #[inline]
318    fn encode(&self, out: &mut Vec<u8>) {
319        self.0.encode(out);
320    }
321}
322
323impl<const W: u32, const F: u32> FixValue for FixedPrice<W, F> {
324    #[inline]
325    fn encode(&self, out: &mut Vec<u8>) {
326        let mut raw = self.raw();
327        if raw < 0 {
328            out.push(b'-');
329            raw = raw.wrapping_neg();
330        }
331
332        let scale_digits = F as usize;
333        if scale_digits == 0 {
334            push_u64_ascii(out, raw as u64);
335            return;
336        }
337        if scale_digits > 18 {
338            out.extend_from_slice(self.to_string().as_bytes());
339            return;
340        }
341
342        let scale = 10u64.pow(F);
343        let abs = raw as u64;
344        let int_part = abs / scale;
345        let frac_part = abs % scale;
346
347        push_u64_ascii(out, int_part);
348        if frac_part == 0 {
349            return;
350        }
351
352        out.push(b'.');
353        let mut frac_buf = [b'0'; 18];
354        let mut x = frac_part;
355        for i in (0..scale_digits).rev() {
356            frac_buf[i] = b'0' + (x % 10) as u8;
357            x /= 10;
358        }
359        let mut end = scale_digits;
360        while end > 0 && frac_buf[end - 1] == b'0' {
361            end -= 1;
362        }
363        out.extend_from_slice(&frac_buf[..end]);
364    }
365}
366
367// --- small helpers (no fmt, no allocation) ---
368
369#[inline]
370fn push_2_u16(out: &mut Vec<u8>, pair: u16) {
371    out.reserve(2);
372    let start = out.len();
373
374    // Get 2 bytes of spare capacity
375    let spare = out.spare_capacity_mut();
376    let dst = &mut spare[..2];
377
378    let bytes = pair.to_ne_bytes();
379    dst[0].write(bytes[0]);
380    dst[1].write(bytes[1]);
381
382    unsafe { out.set_len(start + 2) };
383}
384
385#[inline]
386fn push_2(out: &mut Vec<u8>, n: u8) {
387    push_2_u16(out, DIGITS_U16[n as usize]);
388}
389
390#[inline]
391fn push_3(out: &mut Vec<u8>, n: u16) {
392    out.push(b'0' + (n / 100) as u8);
393    push_2_u16(out, DIGITS_U16[(n % 100) as usize]);
394}
395
396#[inline]
397fn push_4(out: &mut Vec<u8>, n: u32) {
398    debug_assert!(n < 10_000);
399    push_2_u16(out, DIGITS_U16[(n / 100) as usize]);
400    push_2_u16(out, DIGITS_U16[(n % 100) as usize]);
401}
402
403#[inline]
404fn push_i64_ascii(out: &mut Vec<u8>, value: i64) {
405    if value < 0 {
406        out.push(b'-');
407        // avoids UB for i64::MIN
408        push_u64_ascii(out, value.wrapping_neg() as u64);
409    } else {
410        push_u64_ascii(out, value as u64);
411    }
412}
413
414fn push_u64_ascii(out: &mut Vec<u8>, mut value: u64) {
415    let len = num_digits(value);
416    let start = out.len();
417
418    out.reserve(len);
419    let spare = out.spare_capacity_mut();
420    debug_assert!(spare.len() >= len);
421
422    let mut i = len;
423
424    while value >= 100 {
425        let idx = (value % 100) as usize;
426        i -= 2;
427        let bytes = DIGITS_U16[idx].to_ne_bytes();
428        spare[i].write(bytes[0]);
429        spare[i + 1].write(bytes[1]);
430        value /= 100;
431    }
432
433    if value < 10 {
434        i -= 1;
435        spare[i].write(b'0' + value as u8);
436    } else {
437        let idx = value as usize;
438        i -= 2;
439        let bytes = DIGITS_U16[idx].to_ne_bytes();
440        spare[i].write(bytes[0]);
441        spare[i + 1].write(bytes[1]);
442    }
443
444    debug_assert_eq!(i, 0);
445    unsafe { out.set_len(start + len) };
446}
447
448#[inline]
449fn write_usize_ascii(dst: &mut [u8], mut n: usize) -> usize {
450    let mut tmp = [0u8; 20];
451    let mut i = tmp.len();
452    loop {
453        let digit = (n % 10) as u8;
454        i -= 1;
455        tmp[i] = b'0' + digit;
456        n /= 10;
457        if n == 0 {
458            break;
459        }
460    }
461    let len = tmp.len() - i;
462    dst[..len].copy_from_slice(&tmp[i..]);
463    len
464}
465
466#[inline]
467fn encode_timestamp_utc(dt: &DateTime<Utc>, out: &mut Vec<u8>) {
468    push_4(out, dt.year() as u32);
469    push_2(out, dt.month() as u8);
470    push_2(out, dt.day() as u8);
471    out.push(b'-');
472    push_2(out, dt.hour() as u8);
473    out.push(b':');
474    push_2(out, dt.minute() as u8);
475    out.push(b':');
476    push_2(out, dt.second() as u8);
477    out.push(b'.');
478    push_3(out, dt.timestamp_subsec_millis() as u16);
479}
480
481#[inline]
482fn push_checksum_3(out: &mut Vec<u8>, cksum: u8) {
483    out.push(b'0' + (cksum / 100));
484    out.push(b'0' + ((cksum / 10) % 10));
485    out.push(b'0' + (cksum % 10));
486}
487
488/// Chainable message builder returned by `FixBuilder::begin_with`.
489#[must_use = "Call .finish() to finalize the message (writes 8/9 and 10= checksum)"]
490pub struct FixMsg<'a> {
491    b: &'a mut FixBuilder,
492}
493
494impl<'a> FixMsg<'a> {
495    /// Append a field using an owned value.
496    #[inline]
497    pub fn field<V: FixValue>(self, tag: u32, value: V) -> Self {
498        kv(&mut self.b.buf, tag, &value);
499        self
500    }
501
502    /// Append a field using a borrowed value.
503    #[inline]
504    pub fn field_ref<V: FixValue + ?Sized>(self, tag: u32, value: &V) -> Self {
505        kv(&mut self.b.buf, tag, value);
506        self
507    }
508
509    /// Append a field using a value with a compile-time tag.
510    #[inline]
511    pub fn field_tagged<V: FixTaggedValue>(self, value: V) -> Self {
512        kv(&mut self.b.buf, V::TAG, &value);
513        self
514    }
515
516    /// Append a field using a borrowed value with a compile-time tag.
517    #[inline]
518    pub fn field_tagged_ref<V: FixTaggedValue + ?Sized>(self, value: &V) -> Self {
519        kv(&mut self.b.buf, V::TAG, value);
520        self
521    }
522
523    /// Append a string field.
524    #[inline]
525    pub fn str(self, tag: u32, s: &str) -> Self {
526        kv(&mut self.b.buf, tag, s);
527        self
528    }
529
530    /// Append a raw byte field.
531    #[inline]
532    pub fn bytes(self, tag: u32, b: &[u8]) -> Self {
533        kv(&mut self.b.buf, tag, b);
534        self
535    }
536
537    /// Append a field using fallible encoding (currently only `f64` can fail).
538    #[inline]
539    pub fn try_field<V>(self, tag: u32, value: V) -> Result<Self, FixError>
540    where
541        V: TryFixValue<Error = FixError>,
542    {
543        try_kv(&mut self.b.buf, tag, &value)?;
544        Ok(self)
545    }
546
547    /// Append a field using fallible encoding (currently only `f64` can fail).
548    #[inline]
549    pub fn try_field_ref<V>(self, tag: u32, value: &V) -> Result<Self, FixError>
550    where
551        V: TryFixValue<Error = FixError> + ?Sized,
552    {
553        try_kv(&mut self.b.buf, tag, value)?;
554        Ok(self)
555    }
556
557    /// Append a field using fallible encoding and a compile-time tag.
558    #[inline]
559    pub fn try_field_tagged<V>(self, value: V) -> Result<Self, FixError>
560    where
561        V: FixTaggedValue + TryFixValue<Error = FixError>,
562    {
563        try_kv(&mut self.b.buf, V::TAG, &value)?;
564        Ok(self)
565    }
566
567    /// Append a field using fallible encoding and a borrowed value with a compile-time tag.
568    #[inline]
569    pub fn try_field_tagged_ref<V>(self, value: &V) -> Result<Self, FixError>
570    where
571        V: FixTaggedValue + TryFixValue<Error = FixError> + ?Sized,
572    {
573        try_kv(&mut self.b.buf, V::TAG, value)?;
574        Ok(self)
575    }
576
577    /// Add multiple fields using a writer (useful for optional/looped tags).
578    #[inline]
579    pub fn fields(self, f: impl FnOnce(&mut FixMsgWriter<'_>)) -> Self {
580        let mut w = FixMsgWriter {
581            buf: &mut self.b.buf,
582        };
583        f(&mut w);
584        self
585    }
586
587    /// Add multiple fields using a writer that can fail.
588    #[inline]
589    pub fn try_fields<F>(self, f: F) -> Result<Self, FixError>
590    where
591        F: FnOnce(&mut FixMsgWriter<'_>) -> Result<(), FixError>,
592    {
593        let mut w = FixMsgWriter {
594            buf: &mut self.b.buf,
595        };
596        f(&mut w)?;
597        Ok(self)
598    }
599
600    /// Finalize the message (writes header/body length/checksum) and return bytes.
601    #[inline]
602    pub fn finish(self) -> &'a [u8] {
603        self.b.finish()
604    }
605}
606
607/// Helper used by `fields`/`try_fields` to append fields to a message.
608pub struct FixMsgWriter<'a> {
609    buf: &'a mut Vec<u8>,
610}
611
612impl<'a> FixMsgWriter<'a> {
613    /// Append a field using an owned value.
614    #[inline]
615    pub fn field<V: FixValue>(&mut self, tag: u32, value: V) {
616        kv(self.buf, tag, &value);
617    }
618
619    /// Append a field using a borrowed value.
620    #[inline]
621    pub fn field_ref<V: FixValue + ?Sized>(&mut self, tag: u32, v: &V) {
622        kv(self.buf, tag, v);
623    }
624
625    /// Append a field using a value with a compile-time tag.
626    #[inline]
627    pub fn field_tagged<V: FixTaggedValue>(&mut self, value: V) {
628        kv(self.buf, V::TAG, &value);
629    }
630
631    /// Append a field using a borrowed value with a compile-time tag.
632    #[inline]
633    pub fn field_tagged_ref<V: FixTaggedValue + ?Sized>(&mut self, value: &V) {
634        kv(self.buf, V::TAG, value);
635    }
636
637    /// Append a field by value using fallible encoding (currently only `f64` can fail).
638    #[inline]
639    pub fn try_field<V>(&mut self, tag: u32, v: V) -> Result<(), FixError>
640    where
641        V: TryFixValue<Error = FixError>,
642    {
643        try_kv(self.buf, tag, &v)
644    }
645
646    /// Append a field by reference using fallible encoding (currently only `f64` can fail).
647    #[inline]
648    pub fn try_field_ref<V>(&mut self, tag: u32, v: &V) -> Result<(), FixError>
649    where
650        V: TryFixValue<Error = FixError> + ?Sized,
651    {
652        try_kv(self.buf, tag, v)
653    }
654
655    /// Append a field using fallible encoding and a compile-time tag.
656    #[inline]
657    pub fn try_field_tagged<V>(&mut self, value: V) -> Result<(), FixError>
658    where
659        V: FixTaggedValue + TryFixValue<Error = FixError>,
660    {
661        try_kv(self.buf, V::TAG, &value)
662    }
663
664    /// Append a field using fallible encoding and a borrowed value with a compile-time tag.
665    #[inline]
666    pub fn try_field_tagged_ref<V>(&mut self, value: &V) -> Result<(), FixError>
667    where
668        V: FixTaggedValue + TryFixValue<Error = FixError> + ?Sized,
669    {
670        try_kv(self.buf, V::TAG, value)
671    }
672
673    /// Append a string field.
674    #[inline]
675    pub fn str(&mut self, tag: u32, s: &str) {
676        kv(self.buf, tag, s);
677    }
678    /// Append a raw byte field.
679    #[inline]
680    pub fn bytes(&mut self, tag: u32, b: &[u8]) {
681        kv(self.buf, tag, b);
682    }
683}
684
685/// Builder that encodes FIX messages into an internal buffer.
686pub struct FixBuilder {
687    sender: Vec<u8>,
688    target: Vec<u8>,
689    buf: Vec<u8>,
690    fix_version: Vec<u8>,
691    /// Reserved leading space sized for the worst-case `8=<ver><SOH>9=<len><SOH>` prefix.
692    prefix_space: usize,
693}
694
695impl FixBuilder {
696    /// Create a builder with a default buffer capacity.
697    pub fn new(
698        fix_version: impl Into<String>,
699        sender: impl Into<String>,
700        target: impl Into<String>,
701    ) -> Self {
702        Self::with_capacity(fix_version, sender, target, 1024)
703    }
704
705    /// Create a builder with a specific buffer capacity.
706    pub fn with_capacity(
707        fix_version: impl Into<String>,
708        sender: impl Into<String>,
709        target: impl Into<String>,
710        capacity: usize,
711    ) -> Self {
712        let fix_version = fix_version.into().into_bytes();
713        // Worst-case "8=<ver><SOH>9=<body_len><SOH>" prefix.
714        let prefix_space = 2 + fix_version.len() + 1 + 2 + MAX_BODY_LEN_DIGITS + 1;
715        Self {
716            fix_version,
717            sender: sender.into().into_bytes(),
718            target: target.into().into_bytes(),
719            buf: Vec::with_capacity(capacity.max(prefix_space + 64)),
720            prefix_space,
721        }
722    }
723
724    /// Begin a message with explicit seq/time (session layer supplies seq + dt).
725    pub fn begin_with<MT, SEQ, TS>(&mut self, seq_out: &SEQ, dt: &TS, msg_type: &MT) -> FixMsg<'_>
726    where
727        MT: FixValue + ?Sized,
728        SEQ: FixSeqNum + ?Sized,
729        TS: FixSendingTime + ?Sized,
730    {
731        self.buf.clear();
732        self.buf.resize(self.prefix_space, 0);
733
734        let buf = &mut self.buf;
735        kv(buf, 35, msg_type);
736        kv(buf, 34, seq_out);
737        kv_bytes(buf, 49, &self.sender);
738        kv_bytes(buf, 56, &self.target);
739
740        push_u64_ascii(buf, 52);
741        buf.push(b'=');
742        dt.encode_sending_time(buf);
743        buf.push(SOH);
744        FixMsg { b: self }
745    }
746
747    /// Finalize: patch 8/9, compute checksum, append 10, return the message bytes.
748    pub fn finish(&mut self) -> &[u8] {
749        let body_start = self.prefix_space;
750        let body_end = self.buf.len();
751        debug_assert!(body_end >= body_start);
752
753        let body_len = body_end - body_start;
754
755        // header: "8=<fixver><SOH>9=<len><SOH>"
756        let header_len = 2 + self.fix_version.len() + 1 + 2 + num_digits(body_len) + 1;
757        // By construction: prefix_space was sized for MAX_BODY_LEN_DIGITS,
758        // and num_digits(body_len) <= MAX_BODY_LEN_DIGITS for any usize.
759        debug_assert!(header_len <= self.prefix_space);
760
761        let header_start = body_start - header_len;
762
763        // Write header in-place into reserved space.
764        {
765            let header = &mut self.buf[header_start..body_start];
766            let mut i = 0;
767
768            header[i] = b'8';
769            header[i + 1] = b'=';
770            i += 2;
771
772            header[i..i + self.fix_version.len()].copy_from_slice(&self.fix_version);
773            i += self.fix_version.len();
774
775            header[i] = SOH;
776            i += 1;
777
778            header[i] = b'9';
779            header[i + 1] = b'=';
780            i += 2;
781
782            i += write_usize_ascii(&mut header[i..], body_len);
783
784            header[i] = SOH;
785            i += 1;
786
787            debug_assert_eq!(i, header_len);
788        }
789
790        // Compute checksum over header + body (everything up to, excluding tag 10).
791        let mut sum: u32 = 0;
792        for &b in &self.buf[header_start..body_end] {
793            sum += b as u32;
794        }
795        let cksum = (sum % 256) as u8;
796
797        // Append trailer
798        self.buf.extend_from_slice(b"10=");
799        push_checksum_3(&mut self.buf, cksum);
800        self.buf.push(SOH);
801
802        &self.buf[header_start..]
803    }
804}
805
806// --- internal writers ---
807#[inline]
808fn kv<V: FixValue + ?Sized>(buf: &mut Vec<u8>, tag: u32, value: &V) {
809    push_u64_ascii(buf, tag as u64);
810    buf.push(b'=');
811    value.encode(buf);
812    buf.push(SOH);
813}
814
815#[inline]
816fn try_kv<V: TryFixValue<Error = FixError> + ?Sized>(
817    buf: &mut Vec<u8>,
818    tag: u32,
819    value: &V,
820) -> Result<(), FixError> {
821    let start = buf.len();
822    push_u64_ascii(buf, tag as u64);
823    buf.push(b'=');
824
825    if let Err(e) = value.try_encode(buf) {
826        buf.truncate(start);
827
828        // If the value used "tag=0" as a placeholder, attach the real tag here.
829        return Err(match e {
830            FixError::InvalidValue { tag: 0, ctx } => FixError::InvalidValue { tag, ctx },
831            other => other,
832        });
833    }
834
835    buf.push(SOH);
836    Ok(())
837}
838
839#[inline]
840fn kv_bytes(buf: &mut Vec<u8>, tag: u32, bytes: &[u8]) {
841    push_u64_ascii(buf, tag as u64);
842    buf.push(b'=');
843    buf.extend_from_slice(bytes);
844    buf.push(SOH);
845}
846
847const fn digits_00_99_u16() -> [u16; 100] {
848    let mut out = [0u16; 100];
849    let mut n: usize = 0;
850    while n < 100 {
851        let tens = b'0' + (n as u8 / 10);
852        let ones = b'0' + (n as u8 % 10);
853        out[n] = u16::from_ne_bytes([tens, ones]);
854        n += 1;
855    }
856    out
857}
858
859use crate::FixError;
860use core::ops::DivAssign;
861
862trait UnsignedDigits: Copy + PartialOrd + From<u8> + DivAssign<Self> {}
863
864impl UnsignedDigits for u32 {}
865impl UnsignedDigits for u64 {}
866impl UnsignedDigits for usize {}
867
868#[inline]
869fn num_digits<T>(mut value: T) -> usize
870where
871    T: UnsignedDigits + From<u16>,
872{
873    let ten: T = 10u16.into();
874    let hundred: T = 100u16.into();
875    let thousand: T = 1000u16.into();
876    let ten_thousand: T = 10_000u16.into();
877
878    let mut len = 0usize;
879
880    while value >= ten_thousand {
881        value /= ten_thousand;
882        len += 4;
883    }
884
885    if value < hundred {
886        len += if value < ten { 1 } else { 2 };
887    } else {
888        len += if value < thousand { 3 } else { 4 };
889    }
890
891    len
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::enums::{
898        HandlInst as FixHandlInst, MsgType as FixMsgType, OrdType as FixOrdType, Side as FixSide,
899    };
900    use crate::fix::{DayOfMonth as FixDayOfMonth, Price as FixPrice};
901    use crate::tags;
902    use chrono::{TimeZone, Timelike};
903
904    // ---- Test-only FIX types ----
905
906    fn encode_f64(value: f64) -> String {
907        let mut out = Vec::new();
908        <f64 as TryFixValue>::try_encode(&value, &mut out).expect("test value is finite");
909        String::from_utf8(out).expect("f64 encoding should be ASCII")
910    }
911
912    #[derive(Copy, Clone, Debug)]
913    enum TestMsgType {
914        NewOrderSingle,
915    }
916    impl AsFixStr for TestMsgType {
917        fn as_fix_str(&self) -> &'static str {
918            match self {
919                TestMsgType::NewOrderSingle => "D",
920            }
921        }
922    }
923
924    #[derive(Copy, Clone, Debug)]
925    enum HandlInst {
926        Automated,
927    }
928    impl AsFixStr for HandlInst {
929        fn as_fix_str(&self) -> &'static str {
930            match self {
931                HandlInst::Automated => "1",
932            }
933        }
934    }
935    impl FixTaggedValue for HandlInst {
936        const TAG: u32 = tags::HANDL_INST;
937    }
938
939    #[derive(Copy, Clone, Debug)]
940    enum OrdType {
941        Limit,
942    }
943    impl AsFixStr for OrdType {
944        fn as_fix_str(&self) -> &'static str {
945            match self {
946                OrdType::Limit => "2",
947            }
948        }
949    }
950    impl FixTaggedValue for OrdType {
951        const TAG: u32 = tags::ORD_TYPE;
952    }
953
954    #[derive(Copy, Clone, Debug)]
955    struct ClientOrderId(u64);
956    impl FixValue for ClientOrderId {
957        fn encode(&self, out: &mut Vec<u8>) {
958            push_u64_ascii(out, self.0);
959        }
960    }
961
962    // A minimal fixed-decimal-ish price type: mantissa + scale.
963    #[derive(Copy, Clone, Debug)]
964    struct Price {
965        mantissa: i64,
966        scale: u8,
967    }
968    impl FixValue for Price {
969        fn encode(&self, out: &mut Vec<u8>) {
970            let mut m = self.mantissa;
971            if m < 0 {
972                out.push(b'-');
973                m = -m;
974            }
975            let scale = self.scale as usize;
976            if scale == 0 {
977                push_u64_ascii(out, m as u64);
978                return;
979            }
980
981            const POW10: [u64; 19] = [
982                1,
983                10,
984                100,
985                1_000,
986                10_000,
987                100_000,
988                1_000_000,
989                10_000_000,
990                100_000_000,
991                1_000_000_000,
992                10_000_000_000,
993                100_000_000_000,
994                1_000_000_000_000,
995                10_000_000_000_000,
996                100_000_000_000_000,
997                1_000_000_000_000_000,
998                10_000_000_000_000_000,
999                100_000_000_000_000_000,
1000                1_000_000_000_000_000_000,
1001            ];
1002
1003            let div = POW10[scale];
1004            let um = m as u64;
1005            let int_part = um / div;
1006            let frac_part = um % div;
1007
1008            push_u64_ascii(out, int_part);
1009            out.push(b'.');
1010
1011            let mut tmp = [b'0'; 18];
1012            let mut x = frac_part;
1013            for i in (0..scale).rev() {
1014                tmp[i] = b'0' + (x % 10) as u8;
1015                x /= 10;
1016            }
1017            out.extend_from_slice(&tmp[..scale]);
1018        }
1019    }
1020
1021    // ---- Parsing helpers ----
1022
1023    fn find_field(msg: &[u8], tag: u32) -> Option<&[u8]> {
1024        let tag_s = tag.to_string();
1025        let tag_b = tag_s.as_bytes();
1026        for part in msg.split(|&b| b == SOH) {
1027            if part.is_empty() {
1028                continue;
1029            }
1030            let Some(eq) = part.iter().position(|&b| b == b'=') else {
1031                continue;
1032            };
1033            if &part[..eq] == tag_b {
1034                return Some(&part[eq + 1..]);
1035            }
1036        }
1037        None
1038    }
1039
1040    fn parse_u32_ascii(bytes: &[u8]) -> u32 {
1041        let mut v: u32 = 0;
1042        for &b in bytes {
1043            assert!(b.is_ascii_digit());
1044            v = v * 10 + (b - b'0') as u32;
1045        }
1046        v
1047    }
1048
1049    fn locate_body_bounds(msg: &[u8]) -> (usize, usize) {
1050        // body starts after "9=<len><SOH>"
1051        let mut body_start = None;
1052
1053        let mut idx = 0usize;
1054        for part in msg.split(|&b| b == SOH) {
1055            let part_len = part.len();
1056            if part_len == 0 {
1057                idx += 1;
1058                continue;
1059            }
1060            if let Some(eq) = part.iter().position(|&b| b == b'=')
1061                && &part[..eq] == b"9"
1062            {
1063                body_start = Some(idx + part_len + 1);
1064                break;
1065            }
1066            idx += part_len + 1;
1067        }
1068        let body_start = body_start.expect("tag 9 not found");
1069
1070        // checksum field starts at "10="
1071        let mut checksum_tag_start = None;
1072        let mut idx2 = 0usize;
1073        for part in msg.split(|&b| b == SOH) {
1074            let part_len = part.len();
1075            if part_len == 0 {
1076                idx2 += 1;
1077                continue;
1078            }
1079            if part.starts_with(b"10=") {
1080                checksum_tag_start = Some(idx2);
1081            }
1082            idx2 += part_len + 1;
1083        }
1084        let checksum_tag_start = checksum_tag_start.expect("tag 10 not found");
1085
1086        (body_start, checksum_tag_start)
1087    }
1088
1089    fn verify_body_length(msg: &[u8]) {
1090        let body_len = parse_u32_ascii(find_field(msg, 9).expect("missing 9"));
1091        let (body_start, checksum_tag_start) = locate_body_bounds(msg);
1092        let actual = checksum_tag_start - body_start;
1093        assert_eq!(body_len as usize, actual, "BodyLength mismatch");
1094    }
1095
1096    fn verify_checksum(msg: &[u8]) {
1097        let cksum = parse_u32_ascii(find_field(msg, 10).expect("missing 10"));
1098        let (_body_start, checksum_tag_start) = locate_body_bounds(msg);
1099
1100        let sum: u32 = msg[..checksum_tag_start].iter().map(|&b| b as u32).sum();
1101        let expected = sum % 256;
1102        assert_eq!(cksum, expected, "CheckSum mismatch");
1103    }
1104
1105    fn fixed_dt() -> DateTime<Utc> {
1106        Utc.with_ymd_and_hms(2025, 1, 2, 3, 4, 5)
1107            .unwrap()
1108            .with_nanosecond(678_000_000)
1109            .unwrap()
1110    }
1111
1112    #[derive(Debug, fixlite_derive::FixDeserialize)]
1113    struct RoundTripMessage<'a> {
1114        #[fix(tag = 8)]
1115        begin_string: &'a str,
1116        #[fix(tag = 9)]
1117        body_length: u32,
1118        #[fix(tag = 35)]
1119        msg_type: FixMsgType,
1120        #[fix(tag = 49)]
1121        sender_comp_id: &'a str,
1122        #[fix(tag = 56)]
1123        target_comp_id: &'a str,
1124        #[fix(tag = 34)]
1125        msg_seq_num: u64,
1126        #[fix(tag = 52)]
1127        sending_time: DateTime<Utc>,
1128        #[fix(tag = 21)]
1129        handl_inst: FixHandlInst,
1130        #[fix(tag = 40)]
1131        ord_type: FixOrdType,
1132        #[fix(tag = 44)]
1133        price: FixPrice,
1134        #[fix(tag = 205)]
1135        maturity_day: FixDayOfMonth,
1136        #[fix(tag = 10)]
1137        checksum: u8,
1138    }
1139
1140    // ---- Tests ----
1141
1142    #[test]
1143    fn f64_encode_handles_integers_and_fractions() {
1144        assert_eq!(encode_f64(0.0), "0");
1145        assert_eq!(encode_f64(42.0), "42");
1146        assert_eq!(encode_f64(1.5), "1.5");
1147        assert_eq!(encode_f64(1.25), "1.25");
1148        assert_eq!(encode_f64(1.234375), "1.234375");
1149    }
1150
1151    #[test]
1152    fn f64_encode_handles_leading_fractional_zeros_and_signs() {
1153        assert_eq!(encode_f64(0.001953125), "0.001953125");
1154        assert_eq!(encode_f64(-0.5), "-0.5");
1155        assert_eq!(encode_f64(-1.25), "-1.25");
1156    }
1157
1158    #[test]
1159    fn f64_encode_handles_simple_decimals() {
1160        assert_eq!(encode_f64(0.1), "0.1");
1161        assert_eq!(encode_f64(0.01), "0.01");
1162        assert_eq!(encode_f64(10.01), "10.01");
1163    }
1164
1165    #[test]
1166    fn f64_encode_rounds_and_carries_whole() {
1167        assert_eq!(encode_f64(99_999_999_999_999.97), "100000000000000");
1168    }
1169
1170    #[test]
1171    fn begin_with_finish_produces_valid_header_length_and_checksum() {
1172        let mut b = FixBuilder::new("FIX.4.2", "S", "T");
1173
1174        let dt = fixed_dt();
1175        let seq = 7u32;
1176        let mt = TestMsgType::NewOrderSingle;
1177
1178        let msg = b
1179            .begin_with(&seq, &dt, &mt)
1180            .fields(|m| {
1181                m.field(11, ClientOrderId(123));
1182                m.field(21, HandlInst::Automated);
1183                m.field(40, OrdType::Limit);
1184            })
1185            .finish();
1186
1187        assert!(msg.starts_with(b"8=FIX.4.2\x01"), "Missing BeginString");
1188        assert_eq!(find_field(msg, 35).unwrap(), b"D");
1189        assert_eq!(find_field(msg, 34).unwrap(), b"7");
1190        assert_eq!(find_field(msg, 49).unwrap(), b"S");
1191        assert_eq!(find_field(msg, 56).unwrap(), b"T");
1192
1193        verify_body_length(msg);
1194        verify_checksum(msg);
1195    }
1196
1197    #[test]
1198    fn custom_types_are_encoded_correctly() {
1199        let mut b = FixBuilder::new("FIX.4.2", "SENDER", "TARGET");
1200
1201        let dt = fixed_dt();
1202        let seq = 1u32;
1203        let mt = TestMsgType::NewOrderSingle;
1204
1205        let cl = ClientOrderId(999_001);
1206        let px = Price {
1207            mantissa: 12345,
1208            scale: 2,
1209        };
1210
1211        let msg = b
1212            .begin_with(&seq, &dt, &mt)
1213            .field(tags::CL_ORD_ID, cl)
1214            .field_tagged(HandlInst::Automated)
1215            .field_tagged(OrdType::Limit)
1216            .field(44, px)
1217            .finish();
1218
1219        assert_eq!(find_field(msg, 11).unwrap(), b"999001");
1220        assert_eq!(find_field(msg, 21).unwrap(), b"1");
1221        assert_eq!(find_field(msg, 40).unwrap(), b"2");
1222        assert_eq!(find_field(msg, 44).unwrap(), b"123.45");
1223
1224        verify_body_length(msg);
1225        verify_checksum(msg);
1226    }
1227
1228    #[test]
1229    fn builder_reuse_does_not_leak_previous_fields() {
1230        let mut b = FixBuilder::new("FIX.4.2", "S", "T");
1231
1232        let dt = fixed_dt();
1233        let mt = TestMsgType::NewOrderSingle;
1234
1235        let seq1 = 1u32;
1236        let msg1 = b.begin_with(&seq1, &dt, &mt).str(9999, "LEAKME").finish();
1237
1238        assert!(find_field(msg1, 9999).is_some());
1239
1240        let seq2 = 2u32;
1241        let msg2 = b
1242            .begin_with(&seq2, &dt, &mt)
1243            .field(11, ClientOrderId(1))
1244            .finish();
1245
1246        assert!(
1247            find_field(msg2, 9999).is_none(),
1248            "Field leaked across messages"
1249        );
1250
1251        verify_body_length(msg2);
1252        verify_checksum(msg2);
1253    }
1254
1255    #[test]
1256    fn macro_build_fix_builds_message_and_validates_checksum_and_length() {
1257        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1258
1259        let cl = ClientOrderId(77);
1260        let dt = fixed_dt();
1261
1262        let fix_message = build_fix!(
1263            builder,
1264            42u32,
1265            dt,
1266            TestMsgType::NewOrderSingle,
1267            tags::CL_ORD_ID => cl,
1268            @HandlInst::Automated,
1269            @OrdType::Limit,
1270        );
1271
1272        assert!(fix_message.starts_with(b"8=FIX.4.2\x01"));
1273        assert_eq!(find_field(fix_message, 35).unwrap(), b"D");
1274        assert_eq!(find_field(fix_message, 34).unwrap(), b"42");
1275        assert_eq!(find_field(fix_message, 11).unwrap(), b"77");
1276
1277        verify_body_length(fix_message);
1278        verify_checksum(fix_message);
1279    }
1280
1281    #[test]
1282    fn macro_build_fix_accepts_enum_references() {
1283        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1284
1285        let dt = fixed_dt();
1286        let seq = 7u32;
1287        let fix_message = build_fix!(
1288            builder,
1289            seq,
1290            dt,
1291            FixMsgType::NewOrderSingle,
1292            @FixSide::Buy,
1293            @FixOrdType::Limit,
1294        );
1295
1296        assert_eq!(find_field(fix_message, 35).unwrap(), b"D");
1297        assert_eq!(find_field(fix_message, 34).unwrap(), b"7");
1298        assert_eq!(find_field(fix_message, 54).unwrap(), b"1");
1299        assert_eq!(find_field(fix_message, 40).unwrap(), b"2");
1300
1301        verify_body_length(fix_message);
1302        verify_checksum(fix_message);
1303    }
1304
1305    #[test]
1306    fn macro_build_fix_supports_arrow_and_tagged_values() {
1307        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1308
1309        let dt = fixed_dt();
1310        let fix_message = build_fix!(
1311            builder,
1312            3u32,
1313            dt,
1314            FixMsgType::NewOrderSingle,
1315            tags::SIDE => FixSide::Buy,
1316            tags::ORD_TYPE => FixOrdType::Limit,
1317            @FixHandlInst::Automated,
1318        );
1319
1320        assert_eq!(find_field(fix_message, 35).unwrap(), b"D");
1321        assert_eq!(find_field(fix_message, 34).unwrap(), b"3");
1322        assert_eq!(find_field(fix_message, tags::SIDE).unwrap(), b"1");
1323        assert_eq!(find_field(fix_message, tags::ORD_TYPE).unwrap(), b"2");
1324        assert_eq!(find_field(fix_message, tags::HANDL_INST).unwrap(), b"1");
1325
1326        verify_body_length(fix_message);
1327        verify_checksum(fix_message);
1328    }
1329
1330    #[test]
1331    fn field_tagged_uses_enum_tag_constants() {
1332        let mut b = FixBuilder::new("FIX.4.2", "S", "T");
1333
1334        let dt = fixed_dt();
1335        let seq = 9u32;
1336        let mt = FixMsgType::NewOrderSingle;
1337        let side = FixSide::Buy;
1338        let ord_type = FixOrdType::Limit;
1339
1340        let msg = b
1341            .begin_with(&seq, &dt, &mt)
1342            .field_tagged_ref(&side)
1343            .field_tagged(ord_type)
1344            .finish();
1345
1346        assert_eq!(find_field(msg, tags::SIDE).unwrap(), b"1");
1347        assert_eq!(find_field(msg, tags::ORD_TYPE).unwrap(), b"2");
1348
1349        verify_body_length(msg);
1350        verify_checksum(msg);
1351    }
1352
1353    #[test]
1354    fn builder_round_trip_with_fix_types() {
1355        let mut b = FixBuilder::new("FIX.4.2", "SENDER", "TARGET");
1356
1357        let dt = fixed_dt();
1358        let seq = 42;
1359        let price: FixPrice = "123.4500".parse().unwrap();
1360        let day = FixDayOfMonth(7);
1361
1362        let msg = b
1363            .begin_with(&seq, &dt, &FixMsgType::NewOrderSingle)
1364            .field(21, FixHandlInst::Automated)
1365            .field(40, FixOrdType::Limit)
1366            .field(44, price)
1367            .field(205, day)
1368            .finish();
1369
1370        let parsed = <RoundTripMessage as crate::FixDeserialize>::from_fix(msg).unwrap();
1371        assert_eq!(parsed.begin_string, "FIX.4.2");
1372        assert_eq!(parsed.msg_type, FixMsgType::NewOrderSingle);
1373        assert_eq!(parsed.sender_comp_id, "SENDER");
1374        assert_eq!(parsed.target_comp_id, "TARGET");
1375        assert_eq!(parsed.msg_seq_num, seq);
1376        assert_eq!(parsed.sending_time, dt);
1377        assert_eq!(parsed.handl_inst, FixHandlInst::Automated);
1378        assert_eq!(parsed.ord_type, FixOrdType::Limit);
1379        assert_eq!(parsed.price, price);
1380        assert_eq!(parsed.maturity_day, day);
1381        assert_eq!(
1382            parsed.body_length,
1383            parse_u32_ascii(find_field(msg, 9).unwrap())
1384        );
1385        assert_eq!(
1386            parsed.checksum as u32,
1387            parse_u32_ascii(find_field(msg, 10).unwrap())
1388        );
1389
1390        verify_body_length(msg);
1391        verify_checksum(msg);
1392    }
1393
1394    #[test]
1395    fn fields_closure_is_nicer_for_conditionals_and_loops() {
1396        let mut b = FixBuilder::new("FIX.4.2", "S", "T");
1397
1398        let dt = fixed_dt();
1399        let seq = 100u32;
1400        let mt = TestMsgType::NewOrderSingle;
1401
1402        // pretend these come from a higher layer:
1403        let cl_ord_id = Some(ClientOrderId(777));
1404        let account: Option<&str> = None;
1405
1406        // “extras” are dynamic (e.g. strategy tags / venue-specific tags)
1407        let extras: &[(u32, &str)] = &[
1408            (58, "hello"), // Text
1409            (100, "XNAS"), // ExDestination
1410            (110, "1"),    // MinQty
1411        ];
1412
1413        let msg = b
1414            .begin_with(&seq, &dt, &mt)
1415            // fields() is most ergonomic when you want to:
1416            // - conditionally add fields (lots of if let / match)
1417            // - loop over collections (repeating groups / “optional extras”)
1418            // - avoid repeating .field(...) chains that get ugly with branching
1419            .fields(|m| {
1420                // required-ish fields
1421                m.field(21, HandlInst::Automated);
1422                m.field(40, OrdType::Limit);
1423
1424                // conditional fields without breaking the flow
1425                if let Some(cl) = cl_ord_id {
1426                    m.field(11, cl);
1427                }
1428                if let Some(acct) = account {
1429                    m.str(1, acct); // Account(1)
1430                }
1431
1432                // looped/dynamic fields (nice for repeating groups / “extras”)
1433                for &(tag, val) in extras {
1434                    m.str(tag, val);
1435                }
1436            })
1437            .finish();
1438
1439        // spot-check a couple fields exist
1440        assert_eq!(find_field(msg, 34).unwrap(), b"100");
1441        assert_eq!(find_field(msg, 11).unwrap(), b"777");
1442        assert_eq!(find_field(msg, 58).unwrap(), b"hello");
1443        assert_eq!(find_field(msg, 100).unwrap(), b"XNAS");
1444
1445        verify_body_length(msg);
1446        verify_checksum(msg);
1447    }
1448
1449    #[test]
1450    fn long_fix_version_does_not_overflow_prefix() {
1451        // "FIXT.1.1" is 8 bytes; combined with a large body it would have
1452        // overflowed the old hard-coded HEADER_SPACE = 32. The prefix is now
1453        // sized at construction from fix_version.len() + MAX_BODY_LEN_DIGITS,
1454        // so this must produce a well-formed message.
1455        let mut b = FixBuilder::new("FIXT.1.1", "S", "T");
1456        let dt = fixed_dt();
1457        let seq = 1u32;
1458
1459        // Build a message with a large body to exercise multi-digit body lengths.
1460        let big_text: String = "x".repeat(5000);
1461        let msg = b
1462            .begin_with(&seq, &dt, &FixMsgType::NewOrderSingle)
1463            .field_ref(58, &big_text)
1464            .finish();
1465
1466        assert!(msg.starts_with(b"8=FIXT.1.1\x01"));
1467        verify_body_length(msg);
1468        verify_checksum(msg);
1469    }
1470
1471    #[test]
1472    fn macro_build_fix_supports_fallible_arrow_for_f64() {
1473        // `?tag => val` expands to `try_field_ref(...)?` and requires a
1474        // Result-returning context.
1475        fn build_msg(builder: &mut FixBuilder) -> Result<&[u8], FixError> {
1476            let dt = fixed_dt();
1477            let price = 150.25_f64;
1478            let qty = 100.0_f64;
1479            Ok(build_fix!(
1480                builder,
1481                1u32,
1482                dt,
1483                FixMsgType::NewOrderSingle,
1484                ?tags::PRICE => price,
1485                ?tags::ORDER_QTY => qty,
1486                @FixSide::Buy,
1487            ))
1488        }
1489
1490        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1491        let msg = build_msg(&mut builder).unwrap();
1492
1493        assert_eq!(find_field(msg, tags::PRICE).unwrap(), b"150.25");
1494        assert_eq!(find_field(msg, tags::ORDER_QTY).unwrap(), b"100");
1495        assert_eq!(find_field(msg, tags::SIDE).unwrap(), b"1");
1496        verify_body_length(msg);
1497        verify_checksum(msg);
1498    }
1499
1500    #[test]
1501    fn macro_build_fix_propagates_f64_error_for_nan() {
1502        fn build_msg(builder: &mut FixBuilder) -> Result<&[u8], FixError> {
1503            let dt = fixed_dt();
1504            Ok(build_fix!(
1505                builder,
1506                1u32,
1507                dt,
1508                FixMsgType::NewOrderSingle,
1509                ?tags::PRICE => f64::NAN,
1510            ))
1511        }
1512
1513        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1514        let err = build_msg(&mut builder).unwrap_err();
1515        assert!(matches!(
1516            err,
1517            FixError::InvalidValue {
1518                tag: tags::PRICE,
1519                ..
1520            }
1521        ));
1522    }
1523
1524    #[test]
1525    fn macro_build_fix_accepts_string_via_owned_string() {
1526        // `String` implements `FixValue`, so passing an owned `String` works.
1527        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1528        let dt = fixed_dt();
1529        let cl_ord_id = String::from("ABC123");
1530        let msg = build_fix!(
1531            builder, 1u32, dt, FixMsgType::NewOrderSingle,
1532            tags::CL_ORD_ID => cl_ord_id,
1533            @FixSide::Buy,
1534        );
1535        assert_eq!(find_field(msg, tags::CL_ORD_ID).unwrap(), b"ABC123");
1536    }
1537
1538    #[test]
1539    fn macro_build_fix_accepts_string_via_explicit_deref() {
1540        // `&str` does not implement FixValue, but the unsized `str` does.
1541        // Dereffing produces `&str` as the value position, expanding to
1542        // `field_ref(tag, &*str_ref)` = `field_ref::<str>(tag, &str)`.
1543        let mut builder = FixBuilder::new("FIX.4.2", "S", "T");
1544        let dt = fixed_dt();
1545        let cl_ord_id: &str = "ABC123";
1546        let msg = build_fix!(
1547            builder, 1u32, dt, FixMsgType::NewOrderSingle,
1548            tags::CL_ORD_ID => *cl_ord_id,
1549            @FixSide::Buy,
1550        );
1551        assert_eq!(find_field(msg, tags::CL_ORD_ID).unwrap(), b"ABC123");
1552    }
1553
1554    #[test]
1555    fn try_field_f64_rejects_nan() {
1556        let mut b = FixBuilder::new("FIX.4.2", "S", "T");
1557        let dt = fixed_dt();
1558        let seq = 1u32;
1559        let mt = TestMsgType::NewOrderSingle;
1560
1561        let err = match b.begin_with(&seq, &dt, &mt).try_field_ref(44, &f64::NAN) {
1562            Ok(_) => panic!("expected error"),
1563            Err(e) => e,
1564        };
1565
1566        assert!(matches!(err, FixError::InvalidValue { tag: 44, .. }));
1567    }
1568}