easyfix-messages 0.5.4

Easy FIX (Financial Information Exchange) toolset - messages.
Documentation
use std::io::Write;

use tracing::warn;

use crate::fields::basic_types::*;

// TODO: This should be parametrizable and also used in parser to cut too big messages.
const MAX_MSG_SIZE: usize = 4096;
const MAX_BODY_LEN_DIGITS: usize = if MAX_MSG_SIZE < 10000 {
    4
} else if MAX_MSG_SIZE < 100000 {
    5
} else if MAX_MSG_SIZE < 1000000 {
    6
} else if MAX_MSG_SIZE < 10000000 {
    7
} else if MAX_MSG_SIZE < 100000000 {
    8
} else {
    panic!("MAX_MSG_SIZE too big");
};

// TODO: SerializeError: Empty Vec/Group, `0` on SeqNum,TagNum,NumInGroup,Length

impl Default for Serializer {
    fn default() -> Self {
        Self::new()
    }
}

pub struct Serializer {
    output: Vec<u8>,
    body_start_idx: usize,
    current_tag_num: TagNum,
}

impl Serializer {
    pub fn new() -> Serializer {
        Serializer {
            // Allocate for max message size, to prevent vector reallocation.
            output: Vec::with_capacity(MAX_MSG_SIZE),
            body_start_idx: 0,
            current_tag_num: 0,
        }
    }

    pub fn output_mut(&mut self) -> &mut Vec<u8> {
        &mut self.output
    }

    pub fn take(self) -> Vec<u8> {
        self.output
    }

    pub fn serialize_body_len(&mut self) {
        const BODY_LEN_PLACEHOLDER: &[u8] = match MAX_BODY_LEN_DIGITS {
            4 => b"9=0000\x01",
            5 => b"9=00000\x01",
            _ => panic!("unexpected count of maximum body length digits"),
        };
        self.output.extend_from_slice(BODY_LEN_PLACEHOLDER);
        self.body_start_idx = self.output.len();
    }

    // TODO: add test cases for body len and checksum verification
    pub fn serialize_checksum(&mut self) {
        let mut buffer = itoa::Buffer::new();

        let body = &self.output[self.body_start_idx..];
        let body_len = body.len();
        let body_len_slice = buffer.format(body_len).as_bytes();

        self.output[self.body_start_idx - body_len_slice.len() - 1..self.body_start_idx - 1]
            .copy_from_slice(body_len_slice);

        let checksum = self
            .output
            .iter()
            .fold(0u8, |acc, &byte| u8::wrapping_add(acc, byte));

        self.output.extend_from_slice(b"10=");
        if checksum < 10 {
            self.output.extend_from_slice(b"00");
        } else if checksum < 100 {
            self.output.extend_from_slice(b"0");
        }
        self.output
            .extend_from_slice(buffer.format(checksum).as_bytes());
        self.output.push(b'\x01');
    }

    /// Serialize sequence of character digits without commas or decimals.
    /// Value must be positive and may not contain leading zeros.
    pub fn serialize_tag_num(&mut self, tag_num: &TagNum) {
        self.current_tag_num = *tag_num;
        let mut buffer = itoa::Buffer::new();
        self.output
            .extend_from_slice(buffer.format(*tag_num).as_bytes());
    }

    /// Serialize sequence of character digits without commas or decimals
    /// and optional sign character (characters “-” and “0” – “9” ).
    /// The sign character utilizes one octet (i.e., positive int is “99999”
    /// while negative int is “-99999”).
    pub fn serialize_int(&mut self, int: &Int) {
        let mut buffer = itoa::Buffer::new();
        self.output
            .extend_from_slice(buffer.format(*int).as_bytes());
    }

    /// Serialize sequence of character digits without commas or decimals.
    /// Value must be positive.
    pub fn serialize_seq_num(&mut self, seq_num: &SeqNum) {
        let mut buffer = itoa::Buffer::new();
        self.output
            .extend_from_slice(buffer.format(*seq_num).as_bytes());
    }

    /// Serialize sequence of character digits without commas or decimals.
    /// Value must be positive.
    pub fn serialize_num_in_group(&mut self, num_in_group: &NumInGroup) {
        if *num_in_group == 0 {
            warn!("empty Group (tag={})", self.current_tag_num);
        }
        let mut buffer = itoa::Buffer::new();
        self.output
            .extend_from_slice(buffer.format(*num_in_group).as_bytes());
    }

    /// Serialize sequence of character digits without commas or decimals
    /// (values 1 to 31).
    pub fn serialize_day_of_month(&mut self, day_of_month: DayOfMonth) {
        let mut buffer = itoa::Buffer::new();
        self.output
            .extend_from_slice(buffer.format(day_of_month).as_bytes());
    }

    /// Serialize sequence of character digits with optional decimal point
    /// and sign character (characters “-”, “0” – “9” and “.”);
    /// the absence of the decimal point within the string will be interpreted
    /// as the float representation of an integer value. Note that float values
    /// may contain leading zeros (e.g. “00023.23” = “23.23”) and may contain
    /// or omit trailing zeros after the decimal point
    /// (e.g. “23.0” = “23.0000” = “23” = “23.”).
    ///
    /// All float fields must accommodate up to fifteen significant digits.
    /// The number of decimal places used should be a factor of business/market
    /// needs and mutual agreement between counterparties.
    pub fn serialize_float(&mut self, float: &Float) {
        self.output.extend_from_slice(float.to_string().as_bytes())
    }

    pub fn serialize_qty(&mut self, qty: &Qty) {
        self.serialize_float(qty)
    }

    pub fn serialize_price(&mut self, price: &Price) {
        self.serialize_float(price)
    }

    pub fn serialize_price_offset(&mut self, price_offset: &PriceOffset) {
        self.serialize_float(price_offset)
    }

    pub fn serialize_amt(&mut self, amt: &Amt) {
        self.serialize_float(amt)
    }

    pub fn serialize_percentage(&mut self, percentage: &Percentage) {
        self.serialize_float(percentage)
    }

    pub fn serialize_boolean(&mut self, boolean: &Boolean) {
        if *boolean {
            self.output.extend_from_slice(b"Y");
        } else {
            self.output.extend_from_slice(b"N");
        }
    }

    fn validate_char(&self, c: &Char) {
        if matches!(c, 0x00..=0x1f) {
            warn!(
                "wrong Char value - special character ({:#04x}) (tag={})",
                c, self.current_tag_num
            );
        }
        if matches!(c, 0x80..=0xff) {
            warn!(
                "value of Char value - outside ASCII range ({:#04x}) (tag={})",
                c, self.current_tag_num
            );
        }
    }

    /// Use any ASCII character except control characters.
    pub fn serialize_char(&mut self, c: &Char) {
        self.validate_char(c);
        self.output.push(*c);
    }

    /// Serialize string containing one or more space-delimited single
    /// character values, e.g. “2 A F”.
    pub fn serialize_multiple_char_value(&mut self, mcv: &MultipleCharValue) {
        if mcv.is_empty() {
            warn!("empty MutlipleCharValue (tag={})", self.current_tag_num);
        }
        for c in mcv {
            self.validate_char(c);
            self.output.push(*c);
            self.output.push(b' ');
        }
        self.output.pop();
    }

    /// Serialize alphanumeric free-format strings can include any character
    /// except control characters.
    pub fn serialize_string(&mut self, input: &FixStr) {
        if input.is_empty() {
            warn!("empty String (tag={})", self.current_tag_num);
        }
        self.output.extend_from_slice(input.as_bytes());
    }

    /// Serialize string containing one or more space-delimited multiple
    /// character values, e.g. “AV AN A”.
    pub fn serialize_multiple_string_value(&mut self, input: &MultipleStringValue) {
        if input.is_empty() {
            warn!("empty MultipleStringValue (tag={})", self.current_tag_num);
        }
        for s in input {
            if s.is_empty() {
                warn!(
                    "empty MultipleStringValue element (tag={})",
                    self.current_tag_num
                );
            }
            self.output.extend_from_slice(s.as_bytes());
            self.output.push(b' ');
        }
        self.output.pop();
    }

    /// Serialize ISO 3166-1:2013 Codes for the representation of names of
    /// countries and their subdivision (2-character code).
    pub fn serialize_country(&mut self, country: &Country) {
        self.output.extend_from_slice(country.to_bytes());
    }

    /// Serialize ISO 4217:2015 Codes for the representation of currencies
    /// and funds (3-character code).
    pub fn serialize_currency(&mut self, currency: &Currency) {
        self.output.extend_from_slice(currency.to_bytes());
    }

    /// Serialize ISO 10383:2012 Securities and related financial instruments
    /// – Codes for exchanges and market identification (MIC)
    /// (4-character code).
    pub fn serialize_exchange(&mut self, exchange: &Exchange) {
        self.output.extend_from_slice(exchange);
    }

    /// Serialize string representing month of a year.
    /// An optional day of the month can be appended or an optional week code.
    ///
    /// # Valid formats:
    /// YYYYMM
    /// YYYYMMDD
    /// YYYYMMWW
    ///
    /// # Valid values:
    /// YYYY = 0000-9999; MM = 01-12; DD = 01-31;
    /// WW = w1, w2, w3, w4, w5.
    pub fn serialize_month_year(&mut self, input: &MonthYear) {
        self.output.extend_from_slice(input);
    }

    /// Serialize ISO 639-1:2002 Codes for the representation of names
    /// of languages (2-character code).
    pub fn serialize_language(&mut self, input: &Language) {
        self.output.extend_from_slice(input);
    }

    /// Serialize string representing time/date combination represented
    /// in UTC (Universal Time Coordinated) in either YYYYMMDD-HH:MM:SS
    /// (whole seconds) or YYYYMMDD-HH:MM:SS.sss* format, colons, dash,
    /// and period required.
    ///
    /// # Valid values:
    /// - YYYY = 0000-9999,
    /// - MM = 01-12,
    /// - DD = 01-31,
    /// - HH = 00-23,
    /// - MM = 0059,
    /// - SS = 00-60 (60 only if UTC leap second),
    /// - sss* fractions of seconds. The fractions of seconds may be empty when
    ///        no fractions of seconds are conveyed (in such a case the period
    ///        is not conveyed), it may include 3 digits to convey
    ///        milliseconds, 6 digits to convey microseconds, 9 digits
    ///        to convey nanoseconds, 12 digits to convey picoseconds;
    pub fn serialize_utc_timestamp(&mut self, input: &UtcTimestamp) {
        write!(self.output, "{}", input.format("%Y%m%d-%H:%M:%S.%f"))
            .expect("UtcTimestamp serialization failed")
    }

    /// Serialize string representing time-only represented in UTC
    /// (Universal Time Coordinated) in either HH:MM:SS (whole seconds)
    /// or HH:MM:SS.sss* (milliseconds) format, colons, and period required.
    ///
    /// This special-purpose field is paired with UTCDateOnly to form a proper
    /// UTCTimestamp for bandwidth-sensitive messages.
    ///
    /// # Valid values:
    /// - HH = 00-23,
    /// - MM = 00-59,
    /// - SS = 00-60 (60 only if UTC leap second),
    /// - sss* fractions of seconds. The fractions of seconds may be empty when
    ///        no fractions of seconds are conveyed (in such a case the period
    ///        is not conveyed), it may include 3 digits to convey
    ///        milliseconds, 6 digits to convey microseconds, 9 digits
    ///        to convey nanoseconds, 12 digits to convey picoseconds;
    ///        // TODO: set precision!
    pub fn serialize_utc_time_only(&mut self, input: &UtcTimeOnly) {
        write!(self.output, "{}", input.format("%H:%M:%S.%f"))
            .expect("UtcTimeOnly serialization failed")
    }

    /// Serialize date represented in UTC (Universal Time Coordinated)
    /// in YYYYMMDD format.
    ///
    /// # Valid values:
    /// - YYYY = 0000-9999,
    /// - MM = 01-12,
    /// - DD = 01-31.
    pub fn serialize_utc_date_only(&mut self, input: &UtcDateOnly) {
        write!(self.output, "{}", input.format("%Y%m%d")).expect("UtcDateOnly serialization failed")
    }

    /// Serialize time local to a market center. Used where offset to UTC
    /// varies throughout the year and the defining market center is identified
    /// in a corresponding field.
    ///
    /// Format is HH:MM:SS where:
    /// - HH = 00-23 hours,
    /// - MM = 00-59 minutes,
    /// - SS = 00-59 seconds.
    ///
    /// In general only the hour token is non-zero.
    pub fn serialize_local_mkt_time(&mut self, input: &LocalMktTime) {
        write!(self.output, "{}", input.format("%H:%M:%S"))
            .expect("LocalMktTime serialization failed")
    }

    /// Serialize date of local market (as opposed to UTC) in YYYYMMDD
    /// format.
    ///
    /// # Valid values:
    /// - YYYY = 0000-9999,
    /// - MM = 01-12,
    /// - DD = 01-31.
    pub fn serialize_local_mkt_date(&mut self, input: &LocalMktDate) {
        write!(self.output, "{}", input.format("%Y%m%d"))
            .expect("LocalMktDate serialization failed")
    }

    /// Serialize string representing a time/date combination representing
    /// local time with an offset to UTC to allow identification of local time
    /// and time zone offset of that time.
    ///
    /// The representation is based on ISO 8601.
    ///
    /// Format is `YYYYMMDD-HH:MM:SS.sss*[Z | [ + | – hh[:mm]]]` where:
    /// - YYYY = 0000 to 9999,
    /// - MM = 01-12,
    /// - DD = 01-31 HH = 00-23 hours,
    /// - MM = 00-59 minutes,
    /// - SS = 00-59 seconds,
    /// - hh = 01-12 offset hours,
    /// - mm = 00-59 offset minutes,
    /// - sss* fractions of seconds. The fractions of seconds may be empty when
    ///        no fractions of seconds are conveyed (in such a case the period
    ///        is not conveyed), it may include 3 digits to convey
    ///        milliseconds, 6 digits to convey microseconds, 9 digits
    ///        to convey nanoseconds, 12 digits to convey picoseconds;
    pub fn serialize_tz_timestamp(&mut self, input: &TzTimestamp) {
        self.output.extend_from_slice(input)
    }

    /// Serialize time of day with timezone. Time represented based on
    /// ISO 8601. This is the time with a UTC offset to allow identification of
    /// local time and time zone of that time.
    ///
    /// Format is `HH:MM[:SS][Z | [ + | – hh[:mm]]]` where:
    /// - HH = 00-23 hours,
    /// - MM = 00-59 minutes,
    /// - SS = 00-59 seconds,
    /// - hh = 01-12 offset hours,
    /// - mm = 00-59 offset minutes.
    pub fn serialize_tz_timeonly(&mut self, input: &TzTimeOnly) {
        self.output.extend_from_slice(input)
    }

    /// Serialize sequence of character digits without commas or decimals.
    pub fn serialize_length(&mut self, length: &Length) {
        let mut buffer = itoa::Buffer::new();
        self.output
            .extend_from_slice(buffer.format(*length).as_bytes());
    }

    /// Serialize raw data with no format or content restrictions,
    /// or a character string encoded as specified by MessageEncoding(347).
    pub fn serialize_data(&mut self, data: &Data) {
        if data.is_empty() {
            warn!("empty Data (tag={})", self.current_tag_num);
        }
        self.output.extend_from_slice(data);
    }

    /// Serialize XML document.
    pub fn serialize_xml(&mut self, xml_data: &XmlData) {
        if xml_data.is_empty() {
            warn!("empty XmlData (tag={})", self.current_tag_num);
        }
        self.output.extend_from_slice(xml_data);
    }

    // fn serialize_tenor(input: &[u8]) -> Result<Tenor, RejectReason>;

    pub fn serialize_enum<T>(&mut self, value: &T)
    where
        T: Copy + Into<&'static [u8]>,
    {
        self.output.extend_from_slice((*value).into());
    }

    pub fn serialize_enum_collection<T>(&mut self, values: &[T])
    where
        T: Copy + Into<&'static [u8]>,
    {
        if values.is_empty() {
            warn!("empty enum collection (tag={})", self.current_tag_num);
        }
        for value in values {
            self.output.extend_from_slice((*value).into());
            self.output.push(b' ');
        }
        // Drop last space
        self.output.pop();
    }
}

trait Serialize: Sized {
    fn serialize(serializer: &mut Serializer) -> Result<Self, Box<dyn std::error::Error>>;
}