1use std::ops::Range;
13use thiserror::Error;
14
15pub type Result<T> = std::result::Result<T, FixError>;
17
18#[derive(Debug, Error)]
20pub enum FixError {
21 #[error("decode error: {0}")]
23 Decode(#[from] DecodeError),
24
25 #[error("encode error: {0}")]
27 Encode(#[from] EncodeError),
28
29 #[error("session error: {0}")]
31 Session(#[from] SessionError),
32
33 #[error("store error: {0}")]
35 Store(#[from] StoreError),
36
37 #[error("io error: {0}")]
39 Io(#[from] std::io::Error),
40}
41
42#[derive(Debug, Error, Clone, PartialEq, Eq)]
44pub enum DecodeError {
45 #[error("incomplete message, need more data")]
47 Incomplete,
48
49 #[error("invalid begin string: expected 8=FIX.x.y")]
51 InvalidBeginString,
52
53 #[error("missing body length field (tag 9)")]
55 MissingBodyLength,
56
57 #[error("invalid body length value")]
59 InvalidBodyLength,
60
61 #[error("missing msg type field (tag 35)")]
63 MissingMsgType,
64
65 #[error("invalid msg type: {0}")]
67 InvalidMsgType(String),
68
69 #[error("checksum mismatch: calculated {calculated}, declared {declared}")]
71 ChecksumMismatch {
72 calculated: u8,
74 declared: u8,
76 },
77
78 #[error("invalid tag format: {0}")]
80 InvalidTag(String),
81
82 #[error("missing required field: tag {tag}")]
84 MissingRequiredField {
85 tag: u32,
87 },
88
89 #[error("invalid field value for tag {tag}: {reason}")]
91 InvalidFieldValue {
92 tag: u32,
94 reason: String,
96 },
97
98 #[error("group count mismatch for tag {count_tag}: expected {expected}, found {actual}")]
100 GroupCountMismatch {
101 count_tag: u32,
103 expected: u32,
105 actual: u32,
107 },
108
109 #[error("invalid utf-8 in field: {0}")]
111 InvalidUtf8(#[from] std::str::Utf8Error),
112
113 #[error("message too large: {size} bytes exceeds maximum {max_size}")]
115 MessageTooLarge {
116 size: usize,
118 max_size: usize,
120 },
121}
122
123#[derive(Debug, Error, Clone, PartialEq, Eq)]
125pub enum EncodeError {
126 #[error("buffer overflow: need {needed} bytes, have {available}")]
128 BufferOverflow {
129 needed: usize,
131 available: usize,
133 },
134
135 #[error("missing required field: tag {tag}")]
137 MissingRequiredField {
138 tag: u32,
140 },
141
142 #[error("invalid field value for tag {tag}: {reason}")]
144 InvalidFieldValue {
145 tag: u32,
147 reason: String,
149 },
150
151 #[error("field value too long for tag {tag}: {length} exceeds max {max_length}")]
153 FieldTooLong {
154 tag: u32,
156 length: usize,
158 max_length: usize,
160 },
161}
162
163#[derive(Debug, Error, Clone, PartialEq, Eq)]
165pub enum SessionError {
166 #[error("invalid session state: expected {expected}, current {current}")]
168 InvalidState {
169 expected: String,
171 current: String,
173 },
174
175 #[error("logon rejected: {reason}")]
177 LogonRejected {
178 reason: String,
180 },
181
182 #[error("heartbeat timeout after {elapsed_ms} milliseconds")]
184 HeartbeatTimeout {
185 elapsed_ms: u64,
187 },
188
189 #[error("sequence gap detected: expected {expected}, received {received}")]
191 SequenceGap {
192 expected: u64,
194 received: u64,
196 },
197
198 #[error("sequence too low: expected >= {expected}, received {received}")]
200 SequenceTooLow {
201 expected: u64,
203 received: u64,
205 },
206
207 #[error("message rejected: ref_seq={ref_seq_num}, reason={reason}")]
209 MessageRejected {
210 ref_seq_num: u64,
212 reason: String,
214 },
215
216 #[error("resend request for unavailable range: {begin}..{end}")]
218 ResendUnavailable {
219 begin: u64,
221 end: u64,
223 },
224
225 #[error("configuration error: {0}")]
227 Configuration(String),
228
229 #[error("connection error: {0}")]
231 Connection(String),
232}
233
234#[derive(Debug, Error, Clone, PartialEq, Eq)]
236pub enum StoreError {
237 #[error("failed to store message seq={seq_num}: {reason}")]
239 StoreFailed {
240 seq_num: u64,
242 reason: String,
244 },
245
246 #[error("failed to retrieve message seq={seq_num}: {reason}")]
248 RetrieveFailed {
249 seq_num: u64,
251 reason: String,
253 },
254
255 #[error("message not found: seq={seq_num}")]
257 NotFound {
258 seq_num: u64,
260 },
261
262 #[error("messages not available for range: {range:?}")]
264 RangeNotAvailable {
265 range: Range<u64>,
267 },
268
269 #[error("store corrupted: {reason}")]
271 Corrupted {
272 reason: String,
274 },
275
276 #[error("store i/o error: {0}")]
278 Io(String),
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_decode_error_display() {
287 let err = DecodeError::ChecksumMismatch {
288 calculated: 100,
289 declared: 200,
290 };
291 assert_eq!(
292 err.to_string(),
293 "checksum mismatch: calculated 100, declared 200"
294 );
295 }
296
297 #[test]
298 fn test_fix_error_from_decode() {
299 let decode_err = DecodeError::Incomplete;
300 let fix_err: FixError = decode_err.into();
301 assert!(matches!(fix_err, FixError::Decode(DecodeError::Incomplete)));
302 }
303
304 #[test]
305 fn test_session_error_display() {
306 let err = SessionError::SequenceGap {
307 expected: 5,
308 received: 10,
309 };
310 assert_eq!(
311 err.to_string(),
312 "sequence gap detected: expected 5, received 10"
313 );
314 }
315
316 #[test]
317 fn test_store_error_display() {
318 let err = StoreError::NotFound { seq_num: 42 };
319 assert_eq!(err.to_string(), "message not found: seq=42");
320 }
321}