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