use bytes::{BufMut, Bytes, BytesMut};
use crate::{
constants,
digest::Digest,
message::{Body, Header, field::Field},
};
const AVERAGE_BYTES_PER_FIELD: usize = 15;
pub(crate) fn encode(header: &Header, body: &Body) -> Bytes {
let regular_fields = encode_regular_fields(header, body);
let message = encode_framing_headers(header, ®ular_fields);
finalize_message(message)
}
#[must_use]
fn encode_regular_fields(header: &Header, body: &Body) -> BytesMut {
let mut message = BytesMut::with_capacity(
(header.fields.len() + body.fields.len() + 1) * AVERAGE_BYTES_PER_FIELD,
);
message.extend_from_slice(
Field::Custom {
tag: 35,
value: header.msg_type.into(),
}
.encode()
.as_ref(),
);
message.put_u8(constants::SOH);
for field in &header.fields {
let mut field_soh = field.encode();
field_soh.push(constants::SOH);
message.extend_from_slice(field_soh.as_ref());
}
for field in &body.fields {
let mut field_soh = field.encode();
field_soh.push(constants::SOH);
message.extend_from_slice(field_soh.as_ref());
}
message
}
#[must_use]
fn encode_framing_headers(header: &Header, regular_fields: &BytesMut) -> BytesMut {
let mut message = BytesMut::with_capacity(regular_fields.len() + (3 * AVERAGE_BYTES_PER_FIELD));
message.extend_from_slice(
Field::Custom {
tag: 8,
value: header.begin_string.into(),
}
.encode()
.as_ref(),
);
message.put_u8(constants::SOH);
message.extend_from_slice(
Field::Custom {
tag: 9,
value: format!("{}", regular_fields.len()).into_bytes(),
}
.encode()
.as_ref(),
);
message.put_u8(constants::SOH);
message.extend_from_slice(regular_fields);
message
}
fn finalize_message(mut message: BytesMut) -> Bytes {
let mut digest = Digest::default();
digest.push(&message);
let mut checksum_soh = Field::Custom {
tag: 10,
value: format!("{}", digest.checksum()).into_bytes(),
}
.encode();
checksum_soh.push(constants::SOH);
message.put(checksum_soh.as_ref());
message.freeze()
}
#[cfg(test)]
mod test {
use bytes::Bytes;
use crate::{
constants,
encoder::encode,
message::{
Body, Header,
field::{
Field,
value::{begin_string::BeginString, msg_type::MsgType},
},
},
};
fn humanize(encoded_message: &Bytes) -> String {
String::from_utf8_lossy(encoded_message).replace(constants::SOH as char, "|")
}
#[test]
fn message_with_minimal_header() {
let header = Header {
begin_string: BeginString::FIX44,
msg_type: MsgType::Logon,
fields: Vec::new(),
};
let body = Body { fields: Vec::new() };
let encoded_message = encode(&header, &body);
insta::assert_snapshot!(humanize(&encoded_message), @"8=FIX.4.4|9=5|35=A|10=180|");
}
#[test]
fn message_with_optional_header_fields() {
let mut header = Header {
begin_string: BeginString::FIX44,
msg_type: MsgType::Logon,
fields: Vec::new(),
};
let body = Body { fields: Vec::new() };
header.fields.push(Field::Custom {
tag: 144,
value: Vec::from(b"value144"),
});
let encoded_message = encode(&header, &body);
insta::assert_snapshot!(humanize(&encoded_message), @"8=FIX.4.4|9=18|35=A|144=value144|10=117|");
}
#[test]
fn message_with_header_and_body_fields() {
let mut header = Header {
begin_string: BeginString::FIX44,
msg_type: MsgType::Logon,
fields: Vec::new(),
};
let mut body = Body { fields: Vec::new() };
header.fields.push(Field::Custom {
tag: 144,
value: Vec::from(b"value144"),
});
body.fields.push(Field::Custom {
tag: 1234,
value: Vec::from(b"value1234"),
});
body.fields.push(Field::Custom {
tag: 12345,
value: Vec::from(b"value12345"),
});
let encoded_message = encode(&header, &body);
insta::assert_snapshot!(humanize(&encoded_message), @"8=FIX.4.4|9=50|35=A|144=value144|1234=value1234|12345=value12345|10=185|");
}
}