1use thiserror::Error;
88
89#[derive(Error, Debug)]
120pub enum StreamError {
121 #[error("IO error: {0}")]
123 Io(#[from] std::io::Error),
124
125 #[error("Invalid UTF-8 at line {line}: {message}")]
127 Utf8 { line: usize, message: String },
128
129 #[error("Syntax error at line {line}: {message}")]
131 Syntax { line: usize, message: String },
132
133 #[error("Schema error at line {line}: {message}")]
135 Schema { line: usize, message: String },
136
137 #[error("Invalid header: {0}")]
139 Header(String),
140
141 #[error("Missing %VERSION directive")]
143 MissingVersion,
144
145 #[error("Invalid version: {0}")]
147 InvalidVersion(String),
148
149 #[error("Orphan row at line {line}: {message}")]
151 OrphanRow { line: usize, message: String },
152
153 #[error("Shape mismatch at line {line}: expected {expected} columns, got {got}")]
155 ShapeMismatch {
156 line: usize,
157 expected: usize,
158 got: usize,
159 },
160
161 #[error("Parsing timeout: elapsed {elapsed:?} exceeded limit {limit:?}")]
163 Timeout {
164 elapsed: std::time::Duration,
165 limit: std::time::Duration,
166 },
167}
168
169impl StreamError {
170 #[inline]
172 pub fn syntax(line: usize, message: impl Into<String>) -> Self {
173 Self::Syntax {
174 line,
175 message: message.into(),
176 }
177 }
178
179 #[inline]
181 pub fn schema(line: usize, message: impl Into<String>) -> Self {
182 Self::Schema {
183 line,
184 message: message.into(),
185 }
186 }
187
188 #[inline]
190 pub fn orphan_row(line: usize, message: impl Into<String>) -> Self {
191 Self::OrphanRow {
192 line,
193 message: message.into(),
194 }
195 }
196
197 #[inline]
199 pub fn line(&self) -> Option<usize> {
200 match self {
201 Self::Utf8 { line, .. }
202 | Self::Syntax { line, .. }
203 | Self::Schema { line, .. }
204 | Self::OrphanRow { line, .. }
205 | Self::ShapeMismatch { line, .. } => Some(*line),
206 _ => None,
207 }
208 }
209}
210
211pub type StreamResult<T> = Result<T, StreamError>;
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use std::io;
218
219 #[test]
222 fn test_stream_error_io() {
223 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
224 let err = StreamError::Io(io_err);
225 let display = format!("{}", err);
226 assert!(display.contains("IO error"));
227 assert!(display.contains("file not found"));
228 }
229
230 #[test]
231 fn test_stream_error_utf8() {
232 let err = StreamError::Utf8 {
233 line: 42,
234 message: "invalid byte sequence".to_string(),
235 };
236 let display = format!("{}", err);
237 assert!(display.contains("Invalid UTF-8"));
238 assert!(display.contains("42"));
239 assert!(display.contains("invalid byte sequence"));
240 }
241
242 #[test]
243 fn test_stream_error_syntax() {
244 let err = StreamError::Syntax {
245 line: 10,
246 message: "unexpected token".to_string(),
247 };
248 let display = format!("{}", err);
249 assert!(display.contains("Syntax error"));
250 assert!(display.contains("10"));
251 assert!(display.contains("unexpected token"));
252 }
253
254 #[test]
255 fn test_stream_error_schema() {
256 let err = StreamError::Schema {
257 line: 5,
258 message: "undefined type".to_string(),
259 };
260 let display = format!("{}", err);
261 assert!(display.contains("Schema error"));
262 assert!(display.contains("5"));
263 assert!(display.contains("undefined type"));
264 }
265
266 #[test]
267 fn test_stream_error_header() {
268 let err = StreamError::Header("invalid header format".to_string());
269 let display = format!("{}", err);
270 assert!(display.contains("Invalid header"));
271 assert!(display.contains("invalid header format"));
272 }
273
274 #[test]
275 fn test_stream_error_missing_version() {
276 let err = StreamError::MissingVersion;
277 let display = format!("{}", err);
278 assert!(display.contains("Missing %VERSION"));
279 }
280
281 #[test]
282 fn test_stream_error_invalid_version() {
283 let err = StreamError::InvalidVersion("abc".to_string());
284 let display = format!("{}", err);
285 assert!(display.contains("Invalid version"));
286 assert!(display.contains("abc"));
287 }
288
289 #[test]
290 fn test_stream_error_orphan_row() {
291 let err = StreamError::OrphanRow {
292 line: 25,
293 message: "child without parent".to_string(),
294 };
295 let display = format!("{}", err);
296 assert!(display.contains("Orphan row"));
297 assert!(display.contains("25"));
298 assert!(display.contains("child without parent"));
299 }
300
301 #[test]
302 fn test_stream_error_shape_mismatch() {
303 let err = StreamError::ShapeMismatch {
304 line: 100,
305 expected: 5,
306 got: 3,
307 };
308 let display = format!("{}", err);
309 assert!(display.contains("Shape mismatch"));
310 assert!(display.contains("100"));
311 assert!(display.contains("5"));
312 assert!(display.contains("3"));
313 }
314
315 #[test]
318 fn test_syntax_constructor() {
319 let err = StreamError::syntax(15, "invalid syntax");
320 if let StreamError::Syntax { line, message } = err {
321 assert_eq!(line, 15);
322 assert_eq!(message, "invalid syntax");
323 } else {
324 panic!("Expected Syntax variant");
325 }
326 }
327
328 #[test]
329 fn test_syntax_constructor_string() {
330 let err = StreamError::syntax(20, String::from("detailed error"));
331 if let StreamError::Syntax { line, message } = err {
332 assert_eq!(line, 20);
333 assert_eq!(message, "detailed error");
334 } else {
335 panic!("Expected Syntax variant");
336 }
337 }
338
339 #[test]
340 fn test_schema_constructor() {
341 let err = StreamError::schema(30, "type not found");
342 if let StreamError::Schema { line, message } = err {
343 assert_eq!(line, 30);
344 assert_eq!(message, "type not found");
345 } else {
346 panic!("Expected Schema variant");
347 }
348 }
349
350 #[test]
351 fn test_schema_constructor_string() {
352 let err = StreamError::schema(35, String::from("schema validation failed"));
353 if let StreamError::Schema { line, message } = err {
354 assert_eq!(line, 35);
355 assert_eq!(message, "schema validation failed");
356 } else {
357 panic!("Expected Schema variant");
358 }
359 }
360
361 #[test]
362 fn test_orphan_row_constructor() {
363 let err = StreamError::orphan_row(50, "no parent context");
364 if let StreamError::OrphanRow { line, message } = err {
365 assert_eq!(line, 50);
366 assert_eq!(message, "no parent context");
367 } else {
368 panic!("Expected OrphanRow variant");
369 }
370 }
371
372 #[test]
373 fn test_orphan_row_constructor_string() {
374 let err = StreamError::orphan_row(55, String::from("orphan details"));
375 if let StreamError::OrphanRow { line, message } = err {
376 assert_eq!(line, 55);
377 assert_eq!(message, "orphan details");
378 } else {
379 panic!("Expected OrphanRow variant");
380 }
381 }
382
383 #[test]
386 fn test_line_utf8() {
387 let err = StreamError::Utf8 {
388 line: 10,
389 message: "test".to_string(),
390 };
391 assert_eq!(err.line(), Some(10));
392 }
393
394 #[test]
395 fn test_line_syntax() {
396 let err = StreamError::syntax(20, "test");
397 assert_eq!(err.line(), Some(20));
398 }
399
400 #[test]
401 fn test_line_schema() {
402 let err = StreamError::schema(30, "test");
403 assert_eq!(err.line(), Some(30));
404 }
405
406 #[test]
407 fn test_line_orphan_row() {
408 let err = StreamError::orphan_row(40, "test");
409 assert_eq!(err.line(), Some(40));
410 }
411
412 #[test]
413 fn test_line_shape_mismatch() {
414 let err = StreamError::ShapeMismatch {
415 line: 50,
416 expected: 3,
417 got: 2,
418 };
419 assert_eq!(err.line(), Some(50));
420 }
421
422 #[test]
423 fn test_line_io_none() {
424 let io_err = io::Error::other("test");
425 let err = StreamError::Io(io_err);
426 assert_eq!(err.line(), None);
427 }
428
429 #[test]
430 fn test_line_header_none() {
431 let err = StreamError::Header("test".to_string());
432 assert_eq!(err.line(), None);
433 }
434
435 #[test]
436 fn test_line_missing_version_none() {
437 let err = StreamError::MissingVersion;
438 assert_eq!(err.line(), None);
439 }
440
441 #[test]
442 fn test_line_invalid_version_none() {
443 let err = StreamError::InvalidVersion("1.x".to_string());
444 assert_eq!(err.line(), None);
445 }
446
447 #[test]
450 fn test_from_io_error() {
451 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
452 let stream_err: StreamError = io_err.into();
453 assert!(matches!(stream_err, StreamError::Io(_)));
454 let display = format!("{}", stream_err);
455 assert!(display.contains("access denied"));
456 }
457
458 #[test]
459 fn test_from_io_error_not_found() {
460 let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
461 let stream_err: StreamError = io_err.into();
462 assert!(matches!(stream_err, StreamError::Io(_)));
463 }
464
465 #[test]
468 fn test_debug_syntax() {
469 let err = StreamError::syntax(10, "test error");
470 let debug = format!("{:?}", err);
471 assert!(debug.contains("Syntax"));
472 assert!(debug.contains("10"));
473 }
474
475 #[test]
476 fn test_debug_schema() {
477 let err = StreamError::schema(20, "schema issue");
478 let debug = format!("{:?}", err);
479 assert!(debug.contains("Schema"));
480 }
481
482 #[test]
483 fn test_debug_missing_version() {
484 let err = StreamError::MissingVersion;
485 let debug = format!("{:?}", err);
486 assert!(debug.contains("MissingVersion"));
487 }
488
489 #[test]
492 fn test_line_zero() {
493 let err = StreamError::syntax(0, "at start");
494 assert_eq!(err.line(), Some(0));
495 }
496
497 #[test]
498 fn test_line_max() {
499 let err = StreamError::syntax(usize::MAX, "at end");
500 assert_eq!(err.line(), Some(usize::MAX));
501 }
502
503 #[test]
504 fn test_empty_message() {
505 let err = StreamError::syntax(1, "");
506 if let StreamError::Syntax { message, .. } = err {
507 assert!(message.is_empty());
508 }
509 }
510
511 #[test]
512 fn test_unicode_message() {
513 let err = StreamError::syntax(1, "错误信息 🚫");
514 let display = format!("{}", err);
515 assert!(display.contains("错误信息"));
516 assert!(display.contains("🚫"));
517 }
518
519 #[test]
520 fn test_multiline_message() {
521 let err = StreamError::syntax(1, "line1\nline2\nline3");
522 let display = format!("{}", err);
523 assert!(display.contains("line1"));
524 }
525
526 #[test]
527 fn test_shape_mismatch_zero_columns() {
528 let err = StreamError::ShapeMismatch {
529 line: 1,
530 expected: 0,
531 got: 0,
532 };
533 assert_eq!(err.line(), Some(1));
534 }
535
536 #[test]
537 fn test_shape_mismatch_large_numbers() {
538 let err = StreamError::ShapeMismatch {
539 line: 1000000,
540 expected: 1000,
541 got: 999,
542 };
543 let display = format!("{}", err);
544 assert!(display.contains("1000000"));
545 assert!(display.contains("1000"));
546 assert!(display.contains("999"));
547 }
548
549 #[test]
552 fn test_stream_error_timeout() {
553 use std::time::Duration;
554
555 let elapsed = Duration::from_millis(150);
556 let limit = Duration::from_millis(100);
557 let err = StreamError::Timeout { elapsed, limit };
558
559 let display = format!("{}", err);
560 assert!(display.contains("timeout"));
561 assert!(display.contains("150ms"));
562 assert!(display.contains("100ms"));
563 }
564
565 #[test]
566 fn test_timeout_error_debug() {
567 use std::time::Duration;
568
569 let err = StreamError::Timeout {
570 elapsed: Duration::from_secs(1),
571 limit: Duration::from_millis(500),
572 };
573
574 let debug = format!("{:?}", err);
575 assert!(debug.contains("Timeout"));
576 }
577
578 #[test]
579 fn test_timeout_error_no_line() {
580 use std::time::Duration;
581
582 let err = StreamError::Timeout {
583 elapsed: Duration::from_millis(200),
584 limit: Duration::from_millis(100),
585 };
586
587 assert_eq!(err.line(), None);
589 }
590
591 #[test]
592 fn test_timeout_elapsed_greater_than_limit() {
593 use std::time::Duration;
594
595 let elapsed = Duration::from_secs(5);
596 let limit = Duration::from_secs(1);
597 let err = StreamError::Timeout { elapsed, limit };
598
599 let display = format!("{}", err);
600 assert!(display.contains("5s"));
601 assert!(display.contains("1s"));
602 }
603
604 #[test]
605 fn test_timeout_with_nanoseconds() {
606 use std::time::Duration;
607
608 let elapsed = Duration::from_nanos(1500);
609 let limit = Duration::from_nanos(1000);
610 let err = StreamError::Timeout { elapsed, limit };
611
612 let display = format!("{}", err);
613 assert!(display.contains("timeout"));
614 }
615}