truefix-core 0.1.4

FIX message model, field types, and SOH codec (BodyLength/CheckSum).
Documentation
//! An ordered collection of fields and nested repeating groups.

use crate::field::Field;
use crate::group::Group;

/// One member of a [`FieldMap`]: either a plain field or a repeating group.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Member {
    /// A plain field.
    Field(Field),
    /// A repeating group: its count tag and ordered entries.
    Group {
        /// The NoXxx count tag.
        count_tag: u32,
        /// The group entries (each a `FieldMap`).
        entries: Vec<FieldMap>,
        /// NEW-22 (feature 009): the count declared on the wire, if decoded from one and it
        /// didn't match `entries.len()` — see `Group::declared_count`'s doc.
        declared_count: Option<i64>,
    },
}

/// An ordered map of FIX fields (and nested groups), preserving wire order.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FieldMap {
    members: Vec<Member>,
}

impl FieldMap {
    /// Create an empty field map.
    pub fn new() -> Self {
        Self::default()
    }

    /// Append a field, preserving insertion order (used by the decoder).
    pub fn add_field(&mut self, field: Field) {
        self.members.push(Member::Field(field));
    }

    /// Set a top-level field, replacing an existing one with the same tag (in place, preserving
    /// its position), else appending. If more than one field with this tag is already present
    /// (NEW-81, feature 009 -- e.g. because `add_field` was used to push duplicates directly,
    /// bypassing this method's usual dedup), every stale copy is removed, leaving only the new
    /// value at the first occurrence's position.
    pub fn set(&mut self, field: Field) {
        let tag = field.tag();
        let mut replaced = false;
        self.members.retain_mut(|m| {
            let Member::Field(existing) = m else {
                return true;
            };
            if existing.tag() != tag {
                return true;
            }
            if replaced {
                return false;
            }
            *existing = field.clone();
            replaced = true;
            true
        });
        if !replaced {
            self.members.push(Member::Field(field));
        }
    }

    /// Get the first top-level field with `tag`.
    pub fn get(&self, tag: u32) -> Option<&Field> {
        self.members.iter().find_map(|m| match m {
            Member::Field(f) if f.tag() == tag => Some(f),
            _ => None,
        })
    }

    /// Returns `true` if a top-level field with `tag` is present.
    pub fn contains(&self, tag: u32) -> bool {
        self.get(tag).is_some()
    }

    /// Append a repeating group.
    pub fn add_group(&mut self, group: Group) {
        let (count_tag, entries, declared_count) = group.into_parts();
        self.members.push(Member::Group {
            count_tag,
            entries,
            declared_count,
        });
    }

    /// Get the entries of the first group with `count_tag`.
    pub fn group(&self, count_tag: u32) -> Option<&[FieldMap]> {
        self.members.iter().find_map(|m| match m {
            Member::Group {
                count_tag: ct,
                entries,
                ..
            } if *ct == count_tag => Some(entries.as_slice()),
            _ => None,
        })
    }

    /// Get one entry (by 0-based `index`) of the first group with `count_tag` (US9, feature 005,
    /// FR-024/FR-025). `None` if the group doesn't exist or `index` is out of range.
    pub fn get_group(&self, count_tag: u32, index: usize) -> Option<&FieldMap> {
        self.group(count_tag).and_then(|entries| entries.get(index))
    }

    /// Replace one entry (by 0-based `index`) of the first group with `count_tag` (US9, feature
    /// 005, FR-024/FR-025). No-op if the group doesn't exist or `index` is out of range.
    pub fn replace_group(&mut self, count_tag: u32, index: usize, entry: FieldMap) {
        if let Some(Member::Group {
            count_tag: ct,
            entries,
            ..
        }) = self
            .members
            .iter_mut()
            .find(|m| matches!(m, Member::Group { count_tag: ct, .. } if *ct == count_tag))
        {
            debug_assert_eq!(*ct, count_tag);
            if let Some(slot) = entries.get_mut(index) {
                *slot = entry;
            }
        }
    }

    /// Remove one entry (by 0-based `index`) of the first group with `count_tag` (US9, feature
    /// 005, FR-024/FR-025), shifting later entries down. No-op if the group doesn't exist or
    /// `index` is out of range.
    pub fn remove_group(&mut self, count_tag: u32, index: usize) {
        if let Some(Member::Group {
            count_tag: ct,
            entries,
            declared_count,
        }) = self
            .members
            .iter_mut()
            .find(|m| matches!(m, Member::Group { count_tag: ct, .. } if *ct == count_tag))
        {
            debug_assert_eq!(*ct, count_tag);
            if index < entries.len() {
                entries.remove(index);
                // NEW-22 (feature 009): the entry count just changed -- a stale wire-declared
                // count would otherwise be re-encoded verbatim, now genuinely wrong rather than
                // preserving fidelity to anything real.
                *declared_count = None;
            }
        }
    }

    /// Iterate the top-level fields (skipping repeating groups), in order.
    pub fn fields(&self) -> impl Iterator<Item = &Field> {
        self.members.iter().filter_map(|m| match m {
            Member::Field(f) => Some(f),
            Member::Group { .. } => None,
        })
    }

    /// Iterate every top-level member (feature 011, FR-001) — both plain fields and repeating
    /// groups, in wire order. Unlike [`Self::fields`] (which silently skips `Member::Group`
    /// entries), this is the accessor for callers that need to see a message's real structure —
    /// e.g. a group whose own count tag is itself required at the message level (see
    /// `truefix-dict`'s `present()`), or any caller walking a decoded message generically without
    /// assuming every top-level tag is a plain field.
    pub fn members(&self) -> impl Iterator<Item = MemberRef<'_>> {
        self.members.iter().map(|m| match m {
            Member::Field(f) => MemberRef::Field(f),
            Member::Group {
                count_tag,
                entries,
                declared_count,
            } => MemberRef::Group {
                count_tag: *count_tag,
                entries,
                declared_count: *declared_count,
            },
        })
    }

    /// Internal: ordered members, for the encoder.
    pub(crate) fn raw_members(&self) -> &[Member] {
        &self.members
    }
}

/// A borrowed view of one [`FieldMap`] top-level member, returned by [`FieldMap::members`] —
/// either a plain field or a repeating group's count tag plus its ordered entries.
#[derive(Debug, Clone, Copy)]
pub enum MemberRef<'a> {
    /// A plain field.
    Field(&'a Field),
    /// A repeating group: its count tag, ordered entries, and (per [`crate::Group`]'s doc) the
    /// wire-declared count when it didn't match `entries.len()`.
    Group {
        /// The `NoXxx` count tag.
        count_tag: u32,
        /// The group's ordered entries.
        entries: &'a [FieldMap],
        /// The count declared on the wire, if decoded from one and it didn't match
        /// `entries.len()` (`None` for a group built directly by application code).
        declared_count: Option<i64>,
    },
}