use thiserror::Error;
pub type Result<T> = std::result::Result<T, FixError>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FixError {
#[error("decode error: {0}")]
Decode(#[from] DecodeError),
#[error("encode error: {0}")]
Encode(#[from] EncodeError),
#[error("session error: {0}")]
Session(#[from] SessionError),
#[error("store error: {0}")]
Store(#[from] StoreError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecodeError {
#[error("incomplete message, need more data")]
Incomplete,
#[error("invalid begin string: expected 8=FIX.x.y")]
InvalidBeginString,
#[error("missing body length field (tag 9)")]
MissingBodyLength,
#[error("invalid body length value")]
InvalidBodyLength,
#[error("missing msg type field (tag 35)")]
MissingMsgType,
#[error("checksum mismatch: calculated {calculated}, declared {declared}")]
ChecksumMismatch {
calculated: u8,
declared: u8,
},
#[error("invalid tag format: {0}")]
InvalidTag(String),
#[error("missing required field: tag {tag}")]
MissingRequiredField {
tag: u32,
},
#[error("invalid field value for tag {tag}: {reason}")]
InvalidFieldValue {
tag: u32,
reason: String,
},
#[error("invalid utf-8 in field: {0}")]
InvalidUtf8(#[from] std::str::Utf8Error),
#[error("unterminated field for tag {tag}: missing SOH delimiter")]
UnterminatedField {
tag: u32,
},
#[error(
"data field {data_tag} declares {declared} bytes not terminated by SOH within {available} remaining bytes"
)]
InvalidDataLength {
data_tag: u32,
declared: usize,
available: usize,
},
#[error("range {start}..{end} is out of bounds for a {buffer_len}-byte buffer")]
RangeOutOfBounds {
start: usize,
end: usize,
buffer_len: usize,
},
#[error("invalid msg type (tag 35): {0}")]
InvalidMsgType(#[from] MsgTypeError),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncodeError {
#[error("buffer overflow: need {needed} bytes, have {available}")]
BufferOverflow {
needed: usize,
available: usize,
},
#[error("missing required field: tag {tag}")]
MissingRequiredField {
tag: u32,
},
#[error("invalid field value for tag {tag}: {reason}")]
InvalidFieldValue {
tag: u32,
reason: String,
},
#[error("field value too long for tag {tag}: {length} exceeds max {max_length}")]
FieldTooLong {
tag: u32,
length: usize,
max_length: usize,
},
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SessionError {
#[error("invalid session state: expected {expected}, current {current}")]
InvalidState {
expected: String,
current: String,
},
#[error("logon rejected: {reason}")]
LogonRejected {
reason: String,
},
#[error("heartbeat timeout after {elapsed_ms} milliseconds")]
HeartbeatTimeout {
elapsed_ms: u64,
},
#[error("sequence gap detected: expected {expected}, received {received}")]
SequenceGap {
expected: u64,
received: u64,
},
#[error("sequence too low: expected >= {expected}, received {received}")]
SequenceTooLow {
expected: u64,
received: u64,
},
#[error("message rejected: ref_seq={ref_seq_num}, reason={reason}")]
MessageRejected {
ref_seq_num: u64,
reason: String,
},
#[error("resend request for unavailable range: {begin}..{end}")]
ResendUnavailable {
begin: u64,
end: u64,
},
#[error("configuration error: {0}")]
Configuration(String),
#[error("connection error: {0}")]
Connection(String),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum StoreError {
#[error("failed to store message seq={seq_num}: {reason}")]
StoreFailed {
seq_num: u64,
reason: String,
},
#[error("failed to retrieve message seq={seq_num}: {reason}")]
RetrieveFailed {
seq_num: u64,
reason: String,
},
#[error("message not found: seq={seq_num}")]
NotFound {
seq_num: u64,
},
#[error("messages not available for range: {begin}..={end}")]
RangeNotAvailable {
begin: u64,
end: u64,
},
#[error("invalid range: begin {begin} is above end {end}")]
InvalidRange {
begin: u64,
end: u64,
},
#[error("store corrupted: {reason}")]
Corrupted {
reason: String,
},
#[error("store i/o error: {0}")]
Io(String),
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CompIdError {
#[error("comp id is empty")]
Empty,
#[error("comp id is {len} bytes, exceeding the {max_len}-byte inline storage bound")]
TooLong {
len: usize,
max_len: usize,
},
#[error(
"comp id contains illegal byte {byte:#04x} at offset {position}: \
only printable ASCII (0x20..=0x7e) is allowed"
)]
IllegalByte {
byte: u8,
position: usize,
},
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MsgTypeError {
#[error("msg type is empty")]
Empty,
#[error("msg type is {len} bytes, exceeding the {max_len}-byte inline storage bound")]
TooLong {
len: usize,
max_len: usize,
},
#[error(
"msg type contains illegal byte {byte:#04x} at offset {position}: \
only printable ASCII (0x20..=0x7e) is allowed"
)]
IllegalByte {
byte: u8,
position: usize,
},
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimestampError {
#[error("{nanos} nanoseconds since the epoch exceeds the maximum {max_nanos}")]
NanosOutOfRange {
nanos: u64,
max_nanos: u64,
},
#[error("{millis} milliseconds since the epoch is not representable in nanoseconds")]
MillisOutOfRange {
millis: u64,
},
#[error("instant at {seconds} seconds from the epoch is outside the representable range")]
InstantOutOfRange {
seconds: i64,
},
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[error("{tag} is not a legal FIX field tag: tags are positive integers starting at 1")]
pub struct InvalidFieldTag {
tag: u32,
}
impl InvalidFieldTag {
#[inline]
#[must_use]
pub const fn new(tag: u32) -> Self {
Self { tag }
}
#[inline]
#[must_use]
pub const fn tag(self) -> u32 {
self.tag
}
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[error("{value:#04x} is not a FIX Side (tag 54) code")]
pub struct InvalidSide {
value: u8,
}
impl InvalidSide {
#[inline]
#[must_use]
pub const fn new(value: u8) -> Self {
Self { value }
}
#[inline]
#[must_use]
pub const fn value(self) -> u8 {
self.value
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("'{value}' is not a known FIX version")]
pub struct UnknownFixVersion {
value: String,
}
impl UnknownFixVersion {
#[inline]
#[must_use]
pub fn new(value: &str) -> Self {
Self {
value: value.to_owned(),
}
}
#[inline]
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_error_display() {
let err = DecodeError::ChecksumMismatch {
calculated: 100,
declared: 200,
};
assert_eq!(
err.to_string(),
"checksum mismatch: calculated 100, declared 200"
);
}
#[test]
fn test_fix_error_from_decode() {
let decode_err = DecodeError::Incomplete;
let fix_err: FixError = decode_err.into();
assert!(matches!(fix_err, FixError::Decode(DecodeError::Incomplete)));
}
#[test]
fn test_session_error_display() {
let err = SessionError::SequenceGap {
expected: 5,
received: 10,
};
assert_eq!(
err.to_string(),
"sequence gap detected: expected 5, received 10"
);
}
#[test]
fn test_store_error_display() {
let err = StoreError::NotFound { seq_num: 42 };
assert_eq!(err.to_string(), "message not found: seq=42");
}
}