1#[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
2use core::panic::Location;
3use core::{
4 convert::Infallible,
5 ops::{FromResidual, Try},
6};
7
8use crate::Report;
9
10#[derive(Debug)]
21enum BombState {
22 Panic,
24 Warn(
26 #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
29 &'static Location<'static>,
30 ),
31 Defused,
33}
34
35impl Default for BombState {
36 #[track_caller]
37 fn default() -> Self {
38 Self::Warn(
39 #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
40 Location::caller(),
41 )
42 }
43}
44
45#[derive(Debug, Default)]
46struct Bomb(BombState);
47
48impl Bomb {
49 const fn panic() -> Self {
50 Self(BombState::Panic)
51 }
52
53 #[track_caller]
54 const fn warn() -> Self {
55 Self(BombState::Warn(
56 #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
57 Location::caller(),
58 ))
59 }
60
61 const fn defuse(&mut self) {
62 self.0 = BombState::Defused;
63 }
64}
65
66impl Drop for Bomb {
67 fn drop(&mut self) {
68 if !cfg!(debug_assertions) {
70 return;
71 }
72
73 match self.0 {
74 BombState::Panic => panic!("ReportSink was dropped without being consumed"),
75 #[cfg_attr(not(feature = "tracing"), expect(clippy::print_stderr))]
76 #[cfg(any(all(not(target_arch = "wasm32"), feature = "std"), feature = "tracing"))]
77 BombState::Warn(location) => {
78 #[cfg(feature = "tracing")]
79 tracing::warn!(
80 target: "error_stack",
81 %location,
82 "`ReportSink` was dropped without being consumed"
83 );
84 #[cfg(not(feature = "tracing"))]
85 eprintln!("`ReportSink` was dropped without being consumed at {location}");
86 }
87 _ => {}
88 }
89 }
90}
91#[must_use]
145pub struct ReportSink<C> {
146 report: Option<Report<[C]>>,
147 bomb: Bomb,
148}
149
150impl<C> ReportSink<C> {
151 #[track_caller]
155 pub const fn new() -> Self {
156 Self {
157 report: None,
158 bomb: Bomb::warn(),
159 }
160 }
161
162 pub const fn new_armed() -> Self {
166 Self {
167 report: None,
168 bomb: Bomb::panic(),
169 }
170 }
171
172 #[track_caller]
186 pub fn append(&mut self, report: impl Into<Report<[C]>>) {
187 let report = report.into();
188
189 match self.report.as_mut() {
190 Some(existing) => existing.append(report),
191 None => self.report = Some(report),
192 }
193 }
194
195 #[track_caller]
211 pub fn capture(&mut self, error: impl Into<Report<C>>) {
212 let report = error.into();
213
214 match self.report.as_mut() {
215 Some(existing) => existing.push(report),
216 None => self.report = Some(report.into()),
217 }
218 }
219
220 #[track_caller]
249 pub fn attempt<T, R>(&mut self, result: Result<T, R>) -> Option<T>
250 where
251 R: Into<Report<C>>,
252 {
253 match result {
254 Ok(value) => Some(value),
255 Err(error) => {
256 self.capture(error);
257 None
258 }
259 }
260 }
261
262 pub fn finish(mut self) -> Result<(), Report<[C]>> {
280 self.bomb.defuse();
281 self.report.map_or(Ok(()), Err)
282 }
283
284 pub fn finish_with<T>(mut self, ok: impl FnOnce() -> T) -> Result<T, Report<[C]>> {
303 self.bomb.defuse();
304 self.report.map_or_else(|| Ok(ok()), Err)
305 }
306
307 pub fn finish_default<T: Default>(mut self) -> Result<T, Report<[C]>> {
326 self.bomb.defuse();
327 self.report.map_or_else(|| Ok(T::default()), Err)
328 }
329
330 pub fn finish_ok<T>(mut self, ok: T) -> Result<T, Report<[C]>> {
349 self.bomb.defuse();
350 self.report.map_or(Ok(ok), Err)
351 }
352}
353
354impl<C> Default for ReportSink<C> {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360#[cfg(nightly)]
361impl<C> FromResidual for ReportSink<C> {
362 fn from_residual(residual: <Self as Try>::Residual) -> Self {
363 match residual {
364 Err(report) => Self {
365 report: Some(report),
366 bomb: Bomb::default(),
367 },
368 }
369 }
370}
371
372#[cfg(nightly)]
373impl<C> Try for ReportSink<C> {
374 type Output = ();
375 type Residual = Result<Infallible, Report<[C]>>;
377
378 fn from_output((): ()) -> Self {
379 Self {
380 report: None,
381 bomb: Bomb::default(),
382 }
383 }
384
385 fn branch(mut self) -> core::ops::ControlFlow<Self::Residual, Self::Output> {
386 self.bomb.defuse();
387 self.report.map_or(
388 core::ops::ControlFlow::Continue(()), |report| core::ops::ControlFlow::Break(Err(report)),
390 )
391 }
392}
393
394#[cfg(test)]
395mod test {
396 use alloc::collections::BTreeSet;
397 use core::fmt::Display;
398
399 use crate::{Report, sink::ReportSink};
400
401 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
402 struct TestError(u8);
403
404 impl Display for TestError {
405 fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
406 fmt.write_str("TestError(")?;
407 core::fmt::Display::fmt(&self.0, fmt)?;
408 fmt.write_str(")")
409 }
410 }
411
412 impl core::error::Error for TestError {}
413
414 #[test]
415 fn add_single() {
416 let mut sink = ReportSink::new();
417
418 sink.append(Report::new(TestError(0)));
419
420 let report = sink.finish().expect_err("should have failed");
421
422 let contexts: BTreeSet<_> = report.current_contexts().collect();
423 assert_eq!(contexts.len(), 1);
424 assert!(contexts.contains(&TestError(0)));
425 }
426
427 #[test]
428 fn add_multiple() {
429 let mut sink = ReportSink::new();
430
431 sink.append(Report::new(TestError(0)));
432 sink.append(Report::new(TestError(1)));
433
434 let report = sink.finish().expect_err("should have failed");
435
436 let contexts: BTreeSet<_> = report.current_contexts().collect();
437 assert_eq!(contexts.len(), 2);
438 assert!(contexts.contains(&TestError(0)));
439 assert!(contexts.contains(&TestError(1)));
440 }
441
442 #[test]
443 fn capture_single() {
444 let mut sink = ReportSink::new();
445
446 sink.capture(TestError(0));
447
448 let report = sink.finish().expect_err("should have failed");
449
450 let contexts: BTreeSet<_> = report.current_contexts().collect();
451 assert_eq!(contexts.len(), 1);
452 assert!(contexts.contains(&TestError(0)));
453 }
454
455 #[test]
456 fn capture_multiple() {
457 let mut sink = ReportSink::new();
458
459 sink.capture(TestError(0));
460 sink.capture(TestError(1));
461
462 let report = sink.finish().expect_err("should have failed");
463
464 let contexts: BTreeSet<_> = report.current_contexts().collect();
465 assert_eq!(contexts.len(), 2);
466 assert!(contexts.contains(&TestError(0)));
467 assert!(contexts.contains(&TestError(1)));
468 }
469
470 #[test]
471 fn new_does_not_panic() {
472 let _sink: ReportSink<TestError> = ReportSink::new();
473 }
474
475 #[cfg(nightly)]
476 #[test]
477 fn try_none() {
478 fn sink() -> Result<(), Report<[TestError]>> {
479 let sink = ReportSink::new();
480
481 sink?;
482
483 Ok(())
484 }
485
486 sink().expect("should not have failed");
487 }
488
489 #[cfg(nightly)]
490 #[test]
491 fn try_single() {
492 fn sink() -> Result<(), Report<[TestError]>> {
493 let mut sink = ReportSink::new();
494
495 sink.append(Report::new(TestError(0)));
496
497 sink?;
498 Ok(())
499 }
500
501 let report = sink().expect_err("should have failed");
502
503 let contexts: BTreeSet<_> = report.current_contexts().collect();
504 assert_eq!(contexts.len(), 1);
505 assert!(contexts.contains(&TestError(0)));
506 }
507
508 #[cfg(nightly)]
509 #[test]
510 fn try_multiple() {
511 fn sink() -> Result<(), Report<[TestError]>> {
512 let mut sink = ReportSink::new();
513
514 sink.append(Report::new(TestError(0)));
515 sink.append(Report::new(TestError(1)));
516
517 sink?;
518 Ok(())
519 }
520
521 let report = sink().expect_err("should have failed");
522
523 let contexts: BTreeSet<_> = report.current_contexts().collect();
524 assert_eq!(contexts.len(), 2);
525 assert!(contexts.contains(&TestError(0)));
526 assert!(contexts.contains(&TestError(1)));
527 }
528
529 #[cfg(nightly)]
530 #[test]
531 fn try_arbitrary_return() {
532 fn sink() -> Result<u8, Report<[TestError]>> {
533 let mut sink = ReportSink::new();
534
535 sink.append(Report::new(TestError(0)));
536
537 sink?;
538 Ok(8)
539 }
540
541 let report = sink().expect_err("should have failed");
542
543 let contexts: BTreeSet<_> = report.current_contexts().collect();
544 assert_eq!(contexts.len(), 1);
545 assert!(contexts.contains(&TestError(0)));
546 }
547
548 #[test]
549 #[should_panic(expected = "without being consumed")]
550 fn panic_on_unused() {
551 #[expect(clippy::unnecessary_wraps)]
552 fn sink() -> Result<(), Report<[TestError]>> {
553 let mut sink = ReportSink::new_armed();
554
555 sink.append(Report::new(TestError(0)));
556
557 Ok(())
558 }
559
560 let _result = sink();
561 }
562
563 #[test]
564 fn panic_on_unused_with_defuse() {
565 fn sink() -> Result<(), Report<[TestError]>> {
566 let mut sink = ReportSink::new_armed();
567
568 sink.append(Report::new(TestError(0)));
569
570 sink?;
571 Ok(())
572 }
573
574 let report = sink().expect_err("should have failed");
575
576 let contexts: BTreeSet<_> = report.current_contexts().collect();
577 assert_eq!(contexts.len(), 1);
578 assert!(contexts.contains(&TestError(0)));
579 }
580
581 #[test]
582 fn finish() {
583 let mut sink = ReportSink::new();
584
585 sink.append(Report::new(TestError(0)));
586 sink.append(Report::new(TestError(1)));
587
588 let report = sink.finish().expect_err("should have failed");
589
590 let contexts: BTreeSet<_> = report.current_contexts().collect();
591 assert_eq!(contexts.len(), 2);
592 assert!(contexts.contains(&TestError(0)));
593 assert!(contexts.contains(&TestError(1)));
594 }
595
596 #[test]
597 fn finish_ok() {
598 let sink: ReportSink<TestError> = ReportSink::new();
599
600 sink.finish().expect("should have succeeded");
601 }
602
603 #[test]
604 fn finish_with() {
605 let mut sink = ReportSink::new();
606
607 sink.append(Report::new(TestError(0)));
608 sink.append(Report::new(TestError(1)));
609
610 let report = sink.finish_with(|| 8).expect_err("should have failed");
611
612 let contexts: BTreeSet<_> = report.current_contexts().collect();
613 assert_eq!(contexts.len(), 2);
614 assert!(contexts.contains(&TestError(0)));
615 assert!(contexts.contains(&TestError(1)));
616 }
617
618 #[test]
619 fn finish_with_ok() {
620 let sink: ReportSink<TestError> = ReportSink::new();
621
622 let value = sink.finish_with(|| 8).expect("should have succeeded");
623 assert_eq!(value, 8);
624 }
625
626 #[test]
627 fn finish_default() {
628 let mut sink = ReportSink::new();
629
630 sink.append(Report::new(TestError(0)));
631 sink.append(Report::new(TestError(1)));
632
633 let report = sink.finish_default::<u8>().expect_err("should have failed");
634
635 let contexts: BTreeSet<_> = report.current_contexts().collect();
636 assert_eq!(contexts.len(), 2);
637 assert!(contexts.contains(&TestError(0)));
638 assert!(contexts.contains(&TestError(1)));
639 }
640
641 #[test]
642 fn finish_default_ok() {
643 let sink: ReportSink<TestError> = ReportSink::new();
644
645 let value = sink.finish_default::<u8>().expect("should have succeeded");
646 assert_eq!(value, 0);
647 }
648
649 #[test]
650 fn finish_with_value() {
651 let mut sink = ReportSink::new();
652
653 sink.append(Report::new(TestError(0)));
654 sink.append(Report::new(TestError(1)));
655
656 let report = sink.finish_ok(8).expect_err("should have failed");
657
658 let contexts: BTreeSet<_> = report.current_contexts().collect();
659 assert_eq!(contexts.len(), 2);
660 assert!(contexts.contains(&TestError(0)));
661 assert!(contexts.contains(&TestError(1)));
662 }
663
664 #[test]
665 fn finish_with_value_ok() {
666 let sink: ReportSink<TestError> = ReportSink::new();
667
668 let value = sink.finish_ok(8).expect("should have succeeded");
669 assert_eq!(value, 8);
670 }
671}