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