whereat/at.rs
1//! The `At<E>` wrapper type for error location tracking.
2//!
3//! This module provides the core [`At<E>`] type that wraps any error with a trace
4//! of source locations. It's the primary API surface for whereat.
5
6use alloc::borrow::Cow;
7use alloc::boxed::Box;
8use alloc::string::{String, ToString};
9use core::fmt;
10use core::hash::{Hash, Hasher};
11use core::panic::Location;
12
13use crate::AtCrateInfo;
14use crate::context::{AtContext, AtContextRef};
15use crate::trace::{AtFrame, AtFrameOwned, AtTrace, AtTraceBoxed};
16
17// ============================================================================
18// At<E> - Core wrapper type
19// ============================================================================
20
21/// An error with location tracking - wraps any error type.
22///
23/// ## Size
24///
25/// `At<E>` is `sizeof(E) + 8` bytes on 64-bit platforms:
26/// - The error `E` is stored inline
27/// - The trace is boxed (8-byte pointer, null when empty)
28///
29/// ## Equality and Hashing
30///
31/// `At<E>` implements `PartialEq`, `Eq`, and `Hash` based **only on the inner
32/// error `E`**, ignoring the trace. The trace is metadata about *where* an
33/// error was created, not *what* the error is.
34///
35/// This means two `At<E>` values are equal if their inner errors are equal,
36/// even if they were created at different source locations:
37///
38/// ```rust
39/// use whereat::at;
40///
41/// #[derive(Debug, PartialEq)]
42/// struct MyError(u32);
43///
44/// let err1 = at(MyError(42)); // Created here
45/// let err2 = at(MyError(42)); // Created on different line
46/// assert_eq!(err1, err2); // Equal because inner errors match
47/// ```
48///
49/// ## Example
50///
51/// ```rust
52/// use whereat::{at, At};
53///
54/// #[derive(Debug)]
55/// enum MyError { Oops }
56///
57/// // Create a traced error using at() function
58/// let err: At<MyError> = at(MyError::Oops);
59/// assert_eq!(err.frame_count(), 1);
60/// ```
61///
62/// ## Note: Avoid `At<At<E>>`
63///
64/// Nesting `At<At<E>>` is supported but unnecessary and wasteful.
65/// Each `At` has its own trace, so nesting allocates two `Box<AtTrace>`
66/// instead of one. Use `.at()` on Results to extend the existing trace:
67///
68/// ```rust
69/// use whereat::{at, At};
70///
71/// #[derive(Debug)]
72/// struct MyError;
73///
74/// // GOOD: Extend existing trace
75/// fn good() -> Result<(), At<MyError>> {
76/// let err: At<MyError> = at(MyError);
77/// Err(err.at()) // Same trace, new location
78/// }
79///
80/// // UNNECESSARY: Creates two separate traces
81/// fn unnecessary() -> At<At<MyError>> {
82/// at(at(MyError)) // Two allocations
83/// }
84/// ```
85pub struct At<E> {
86 error: E,
87 trace: AtTraceBoxed,
88}
89
90// ============================================================================
91// At<E> Implementation
92// ============================================================================
93
94impl<E> At<E> {
95 /// Wrap an error without capturing any location.
96 ///
97 /// Use this when you want to defer tracing until later (e.g., exiting a hot loop).
98 /// Call `.at()` to add the first location when ready.
99 ///
100 /// For normal use, prefer [`at()`](crate::at()) or [`at!()`](crate::at!) which
101 /// capture the caller's location immediately.
102 #[inline]
103 pub const fn wrap(error: E) -> Self {
104 Self {
105 error,
106 trace: AtTraceBoxed::new(),
107 }
108 }
109
110 /// Create an `At<E>` from an error and an existing trace.
111 ///
112 /// Used for transferring traces between error types.
113 #[inline]
114 pub fn from_parts(error: E, trace: AtTrace) -> Self {
115 let mut boxed = AtTraceBoxed::new();
116 boxed.set(trace);
117 Self {
118 error,
119 trace: boxed,
120 }
121 }
122
123 /// Ensure trace exists, creating it if necessary.
124 fn ensure_trace(&mut self) -> &mut AtTrace {
125 self.trace.get_or_insert_mut()
126 }
127
128 /// Add the caller's location to the trace.
129 ///
130 /// This is the primary API for building up a stack trace as errors propagate.
131 /// If allocation fails, the location is silently skipped.
132 ///
133 /// ## Example
134 ///
135 /// ```rust
136 /// use whereat::At;
137 ///
138 /// #[derive(Debug)]
139 /// enum MyError { Oops }
140 ///
141 /// fn inner() -> Result<(), At<MyError>> {
142 /// Err(At::wrap(MyError::Oops).at())
143 /// }
144 ///
145 /// fn outer() -> Result<(), At<MyError>> {
146 /// inner().map_err(|e| e.at())
147 /// }
148 /// ```
149 #[track_caller]
150 #[inline]
151 pub fn at(mut self) -> Self {
152 let loc = Location::caller();
153 let trace = self.trace.get_or_insert_mut();
154 let _ = trace.try_push(loc);
155 self
156 }
157
158 /// Add a location frame with the caller's function name as context.
159 ///
160 /// Captures both file:line:col AND the function name at zero runtime cost.
161 /// Pass an empty closure `|| {}` - its type includes the parent function name.
162 ///
163 /// ## Example
164 ///
165 /// ```rust
166 /// use whereat::{at, At};
167 ///
168 /// #[derive(Debug)]
169 /// enum MyError { NotFound }
170 ///
171 /// fn load_config() -> Result<(), At<MyError>> {
172 /// Err(at(MyError::NotFound).at_fn(|| {}))
173 /// }
174 ///
175 /// // Output will include:
176 /// // at src/lib.rs:10:5
177 /// // in my_crate::load_config
178 /// ```
179 #[track_caller]
180 #[inline]
181 pub fn at_fn<F: Fn()>(mut self, _marker: F) -> Self {
182 let full_name = core::any::type_name::<F>();
183 // Type looks like: "crate::module::function::{{closure}}"
184 // Strip "::{{closure}}" suffix if present
185 let name = full_name.strip_suffix("::{{closure}}").unwrap_or(full_name);
186 let loc = Location::caller();
187 let trace = self.trace.get_or_insert_mut();
188 // First push a new location frame
189 let _ = trace.try_push(loc);
190 // Then add function name context to that frame
191 let context = AtContext::FunctionName(name);
192 trace.try_add_context(loc, context);
193 self
194 }
195
196 /// Add a location frame with an explicit name as context.
197 ///
198 /// Like [`at_fn`](Self::at_fn) but with an explicit label instead of
199 /// auto-detecting the function name. Useful for naming checkpoints,
200 /// phases, or operations within a function.
201 ///
202 /// ## Example
203 ///
204 /// ```rust
205 /// use whereat::{at, At};
206 ///
207 /// #[derive(Debug)]
208 /// enum MyError { Failed }
209 ///
210 /// fn process() -> Result<(), At<MyError>> {
211 /// // ... validation phase ...
212 /// Err(at(MyError::Failed).at_named("validation"))
213 /// }
214 ///
215 /// // Output will include:
216 /// // at src/lib.rs:10:5
217 /// // in validation
218 /// ```
219 #[track_caller]
220 #[inline]
221 pub fn at_named(mut self, name: &'static str) -> Self {
222 let loc = Location::caller();
223 let trace = self.trace.get_or_insert_mut();
224 // Push a new location frame
225 let _ = trace.try_push(loc);
226 // Add the name as function-name-style context
227 let context = AtContext::FunctionName(name);
228 trace.try_add_context(loc, context);
229 self
230 }
231
232 /// Add a static string context to the last location frame.
233 ///
234 /// **Does not add a new location frame** - attaches context to the most recent
235 /// frame in the trace. If the trace is empty, creates a frame at the caller's
236 /// location first.
237 ///
238 /// Zero-cost for static strings - just stores a pointer.
239 /// For dynamically-computed strings, use [`at_string()`](Self::at_string).
240 ///
241 /// ## Frame behavior
242 ///
243 /// ```rust
244 /// use whereat::at;
245 ///
246 /// #[derive(Debug)]
247 /// struct E;
248 ///
249 /// // One frame with two contexts
250 /// let e = at(E).at_str("a").at_str("b");
251 /// assert_eq!(e.frame_count(), 1);
252 ///
253 /// // Two frames: first from at(), second gets the context
254 /// let e = at(E).at().at_str("on second frame");
255 /// assert_eq!(e.frame_count(), 2);
256 /// ```
257 ///
258 /// ## Example
259 ///
260 /// ```rust
261 /// use whereat::{at, At, ResultAtExt};
262 ///
263 /// #[derive(Debug)]
264 /// enum MyError { IoError }
265 ///
266 /// fn read_config() -> Result<(), At<MyError>> {
267 /// Err(at(MyError::IoError))
268 /// }
269 ///
270 /// fn init() -> Result<(), At<MyError>> {
271 /// read_config().at_str("while loading configuration")?;
272 /// Ok(())
273 /// }
274 /// ```
275 #[track_caller]
276 #[inline]
277 pub fn at_str(mut self, msg: &'static str) -> Self {
278 let loc = Location::caller();
279 let context = AtContext::Text(Cow::Borrowed(msg));
280 let trace = self.trace.get_or_insert_mut();
281 trace.try_add_context(loc, context);
282 self
283 }
284
285 /// Add a lazily-computed string context to the last location frame.
286 ///
287 /// **Does not add a new location frame** - attaches context to the most recent
288 /// frame in the trace. If the trace is empty, creates a frame at the caller's
289 /// location first.
290 ///
291 /// The closure is only called on error path, avoiding allocation on success.
292 /// For static strings, use [`at_str()`](Self::at_str) instead for zero overhead.
293 ///
294 /// ## Example
295 ///
296 /// ```rust
297 /// use whereat::{at, At, ResultAtExt};
298 ///
299 /// #[derive(Debug)]
300 /// enum MyError { NotFound }
301 ///
302 /// fn load(path: &str) -> Result<(), At<MyError>> {
303 /// Err(at(MyError::NotFound))
304 /// }
305 ///
306 /// fn init(path: &str) -> Result<(), At<MyError>> {
307 /// // Closure only runs on Err - no allocation on Ok path
308 /// load(path).at_string(|| format!("loading {}", path))?;
309 /// Ok(())
310 /// }
311 /// ```
312 #[track_caller]
313 #[inline]
314 pub fn at_string(mut self, f: impl FnOnce() -> String) -> Self {
315 let loc = Location::caller();
316 let context = AtContext::Text(Cow::Owned(f()));
317 let trace = self.trace.get_or_insert_mut();
318 trace.try_add_context(loc, context);
319 self
320 }
321
322 /// Add lazily-computed typed context (Display) to the last location frame.
323 ///
324 /// **Does not add a new location frame** - attaches context to the most recent
325 /// frame in the trace. If the trace is empty, creates a frame at the caller's
326 /// location first.
327 ///
328 /// The closure is only called on error path, avoiding allocation on success.
329 /// Use for typed data that you want to format with `Display` and later retrieve
330 /// via [`downcast_ref::<T>()`](crate::AtContextRef::downcast_ref).
331 ///
332 /// For plain string messages, prefer [`at_str()`](Self::at_str) or [`at_string()`](Self::at_string).
333 /// For Debug-formatted data, use [`at_debug()`](Self::at_debug).
334 ///
335 /// ## Example
336 ///
337 /// ```rust
338 /// use whereat::{at, At};
339 ///
340 /// #[derive(Debug)]
341 /// enum MyError { NotFound }
342 ///
343 /// // Custom Display type for rich context
344 /// struct PathContext(String);
345 /// impl std::fmt::Display for PathContext {
346 /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 /// write!(f, "path: {}", self.0)
348 /// }
349 /// }
350 ///
351 /// fn load(path: &str) -> Result<(), At<MyError>> {
352 /// Err(at(MyError::NotFound))
353 /// }
354 ///
355 /// fn init(path: &str) -> Result<(), At<MyError>> {
356 /// load(path).map_err(|e| e.at_data(|| PathContext(path.into())))?;
357 /// Ok(())
358 /// }
359 /// ```
360 #[track_caller]
361 #[inline]
362 pub fn at_data<T: fmt::Display + Send + Sync + 'static>(
363 mut self,
364 f: impl FnOnce() -> T,
365 ) -> Self {
366 let loc = Location::caller();
367 let ctx = f();
368 let context = AtContext::Display(Box::new(ctx));
369 let trace = self.trace.get_or_insert_mut();
370 trace.try_add_context(loc, context);
371 self
372 }
373
374 /// Add lazily-computed typed context (Debug) to the last location frame.
375 ///
376 /// **Does not add a new location frame** - attaches context to the most recent
377 /// frame in the trace. If the trace is empty, creates a frame at the caller's
378 /// location first.
379 ///
380 /// The closure is only called on error path, avoiding allocation on success.
381 /// Use [`contexts()`](Self::contexts) to retrieve entries and
382 /// [`downcast_ref()`](crate::AtContextRef::downcast_ref) to access typed data.
383 ///
384 /// ## Example
385 ///
386 /// ```rust
387 /// use whereat::at;
388 ///
389 /// #[derive(Debug)]
390 /// struct RequestInfo { user_id: u64, path: String }
391 ///
392 /// #[derive(Debug)]
393 /// enum MyError { Forbidden }
394 ///
395 /// let err = at(MyError::Forbidden)
396 /// .at_debug(|| RequestInfo { user_id: 42, path: "/admin".into() });
397 ///
398 /// // Later, retrieve the context
399 /// for ctx in err.contexts() {
400 /// if let Some(req) = ctx.downcast_ref::<RequestInfo>() {
401 /// assert_eq!(req.user_id, 42);
402 /// }
403 /// }
404 /// ```
405 #[track_caller]
406 #[inline]
407 pub fn at_debug<T: fmt::Debug + Send + Sync + 'static>(
408 mut self,
409 f: impl FnOnce() -> T,
410 ) -> Self {
411 let loc = Location::caller();
412 let ctx = f();
413 let context = AtContext::Debug(Box::new(ctx));
414 let trace = self.trace.get_or_insert_mut();
415 trace.try_add_context(loc, context);
416 self
417 }
418
419 /// Attach an error as diagnostic context to the last location frame.
420 ///
421 /// **Does not add a new location frame** - attaches context to the most recent
422 /// frame in the trace. If the trace is empty, creates a frame at the caller's
423 /// location first.
424 ///
425 /// Attach a related error as **diagnostic context** on the last frame.
426 ///
427 /// **Does not add a new location frame** — attaches to the most recent frame.
428 /// If the trace is empty, creates a frame at the caller's location first.
429 ///
430 /// The attached error is visible via [`contexts()`](Self::contexts) iteration
431 /// and [`full_trace()`](Self::full_trace) display, but is **not** part of the
432 /// [`Error::source()`] chain. `At<E>::source()` always delegates to `E::source()`.
433 ///
434 /// This is intentional: `.source()` models a linear causal chain ("A was caused
435 /// by B"), while `.at_aside_error()` models an observation ("while handling A,
436 /// we also saw X"). These are different relationships, and forcing the latter
437 /// into a linear chain would be lossy — a trace can have multiple
438 /// `.at_aside_error()` calls at different frames.
439 ///
440 /// If you need an error to appear in the `.source()` chain, store it inside
441 /// your error type `E` directly.
442 ///
443 /// ## Example
444 ///
445 /// ```rust
446 /// use whereat::at;
447 /// use std::io;
448 ///
449 /// #[derive(Debug)]
450 /// struct MyError;
451 ///
452 /// fn wrap_io_error(io_err: io::Error) -> whereat::At<MyError> {
453 /// at(MyError).at_aside_error(io_err)
454 /// }
455 /// ```
456 #[track_caller]
457 #[inline]
458 pub fn at_aside_error<Err: core::error::Error + Send + Sync + 'static>(
459 mut self,
460 err: Err,
461 ) -> Self {
462 let loc = Location::caller();
463 let context = AtContext::Error(Box::new(err));
464 let trace = self.trace.get_or_insert_mut();
465 trace.try_add_context(loc, context);
466 self
467 }
468
469 /// Attach an error as diagnostic context (not in `.source()` chain).
470 ///
471 /// # Deprecated
472 ///
473 /// Renamed to [`at_aside_error()`](Self::at_aside_error) to clarify that the
474 /// attached error is diagnostic context, not part of the standard error chain.
475 /// Code that iterates `.source()` will never see errors attached this way.
476 #[deprecated(
477 since = "0.1.4",
478 note = "Renamed to `at_aside_error()`. The attached error is diagnostic context \
479 only — it is NOT part of the `.source()` chain."
480 )]
481 #[track_caller]
482 #[inline]
483 pub fn at_error<Err: core::error::Error + Send + Sync + 'static>(mut self, err: Err) -> Self {
484 let loc = Location::caller();
485 let context = AtContext::Error(Box::new(err));
486 let trace = self.trace.get_or_insert_mut();
487 trace.try_add_context(loc, context);
488 self
489 }
490
491 /// Add a crate boundary marker to the last location frame.
492 ///
493 /// **Does not add a new location frame** - attaches context to the most recent
494 /// frame in the trace. If the trace is empty, creates a frame at the caller's
495 /// location first.
496 ///
497 /// This marks that subsequent locations belong to a different crate,
498 /// enabling correct GitHub links in cross-crate traces.
499 ///
500 /// Requires [`define_at_crate_info!()`](crate::define_at_crate_info!) or
501 /// a custom `at_crate_info()` getter.
502 ///
503 /// ## Example
504 ///
505 /// ```rust,ignore
506 /// // Requires define_at_crate_info!() setup
507 /// use whereat::{at, At};
508 ///
509 /// whereat::define_at_crate_info!();
510 ///
511 /// #[derive(Debug)]
512 /// enum MyError { Wrapped(String) }
513 ///
514 /// fn wrap_external_error(msg: &str) -> At<MyError> {
515 /// at(MyError::Wrapped(msg.into()))
516 /// .at_crate(crate::at_crate_info())
517 /// }
518 /// ```
519 #[track_caller]
520 #[inline]
521 pub fn at_crate(mut self, info: &'static AtCrateInfo) -> Self {
522 let loc = Location::caller();
523 let trace = self.trace.get_or_insert_mut();
524 trace.try_add_crate_boundary(loc, info);
525 self
526 }
527
528 /// Add a skip marker (`[...]`) to the trace.
529 ///
530 /// Use this to indicate that some frames were skipped, either because
531 /// tracing started late in the call stack or because intermediate frames
532 /// are not meaningful.
533 #[doc(hidden)]
534 #[inline]
535 pub fn at_skipped_frames(mut self) -> Self {
536 let trace = self.trace.get_or_insert_mut();
537 let _ = trace.try_push_skipped();
538 self
539 }
540
541 /// Set the crate info for this trace.
542 ///
543 /// This is used by `at!()` to provide repository metadata for GitHub links.
544 /// Calling this creates the trace if it doesn't exist yet.
545 ///
546 /// ## Example
547 ///
548 /// ```rust,ignore
549 /// // Requires define_at_crate_info!() setup
550 /// whereat::define_at_crate_info!();
551 ///
552 /// #[derive(Debug)]
553 /// enum MyError { Oops }
554 ///
555 /// let err = At::wrap(MyError::Oops)
556 /// .set_crate_info(crate::at_crate_info())
557 /// .at();
558 /// ```
559 #[inline]
560 pub fn set_crate_info(mut self, info: &'static AtCrateInfo) -> Self {
561 let trace = self.trace.get_or_insert_mut();
562 trace.set_crate_info(info);
563 self
564 }
565
566 /// Get the crate info for this trace, if set.
567 #[inline]
568 pub fn crate_info(&self) -> Option<&'static AtCrateInfo> {
569 self.trace.as_ref().and_then(|t| t.crate_info())
570 }
571
572 /// Get a reference to the inner error.
573 #[inline]
574 pub fn error(&self) -> &E {
575 &self.error
576 }
577
578 /// Get a mutable reference to the inner error.
579 #[inline]
580 pub fn error_mut(&mut self) -> &mut E {
581 &mut self.error
582 }
583
584 /// Consume self and return the inner error, **discarding the trace**.
585 ///
586 /// # Deprecated
587 ///
588 /// Use [`decompose()`](Self::decompose) to get both the error and trace,
589 /// or [`map_error()`](Self::map_error) to convert the error type while
590 /// preserving the trace.
591 ///
592 /// ```rust
593 /// use whereat::at;
594 ///
595 /// #[derive(Debug, PartialEq)]
596 /// struct MyError;
597 ///
598 /// let traced = at(MyError);
599 ///
600 /// // Instead of: let err = traced.into_inner(); // trace lost!
601 /// let (err, trace) = traced.decompose(); // trace preserved
602 /// assert_eq!(err, MyError);
603 /// assert!(trace.is_some());
604 /// ```
605 #[deprecated(
606 since = "0.1.4",
607 note = "Discards the trace. Use `decompose()` to get both error and trace, \
608 or `map_error()` to convert types while preserving the trace."
609 )]
610 #[inline]
611 pub fn into_inner(self) -> E {
612 self.error
613 }
614
615 /// Consume self and return both the error and the trace.
616 ///
617 /// This is the recommended way to take apart an `At<E>` without losing
618 /// location information. If you need to convert the error type while
619 /// keeping the trace, use [`map_error()`](Self::map_error) instead.
620 ///
621 /// ```rust
622 /// use whereat::at;
623 ///
624 /// #[derive(Debug, PartialEq)]
625 /// struct MyError;
626 ///
627 /// let traced = at(MyError);
628 /// let (err, trace) = traced.decompose();
629 /// assert_eq!(err, MyError);
630 /// assert!(trace.is_some()); // trace is preserved
631 /// ```
632 #[inline]
633 pub fn decompose(mut self) -> (E, Option<AtTrace>) {
634 let trace = self.trace.take();
635 (self.error, trace)
636 }
637
638 /// Check if the trace is empty.
639 #[inline]
640 pub fn is_empty(&self) -> bool {
641 self.trace.is_empty()
642 }
643
644 /// Iterate over all traced locations, oldest first.
645 ///
646 /// Skipped frame markers (`[...]`) are not included in this iteration.
647 /// Use [`frames()`](Self::frames) for full iteration including contexts.
648 #[inline]
649 #[allow(dead_code)] // Used in tests
650 pub(crate) fn locations(&self) -> impl Iterator<Item = &'static Location<'static>> + '_ {
651 self.trace
652 .as_ref()
653 .into_iter()
654 .flat_map(|t| t.iter())
655 .flatten() // Filter out None (skipped frame markers)
656 }
657
658 /// Get the first (oldest) location in the trace, if any.
659 #[inline]
660 #[allow(dead_code)] // Used in tests
661 pub(crate) fn first_location(&self) -> Option<&'static Location<'static>> {
662 self.locations().next()
663 }
664
665 /// Get the last (most recent) location in the trace, if any.
666 #[inline]
667 #[allow(dead_code)] // Used in tests
668 pub(crate) fn last_location(&self) -> Option<&'static Location<'static>> {
669 self.locations().last()
670 }
671
672 /// Get a reference to the underlying trace, if any.
673 #[inline]
674 #[allow(dead_code)] // Used in format module
675 pub(crate) fn trace_ref(&self) -> Option<&AtTrace> {
676 self.trace.as_ref()
677 }
678
679 /// Iterate over all context entries, newest first.
680 ///
681 /// Each call to `at_str()`, `at_string()`, `at_data()`, or `at_debug()` creates
682 /// a context entry. Use [`AtContextRef`] methods to inspect context data.
683 ///
684 /// **Note:** Prefer [`frames()`](Self::frames) for unified iteration over
685 /// locations with their contexts.
686 ///
687 /// ## Example
688 ///
689 /// ```rust
690 /// use whereat::at;
691 ///
692 /// #[derive(Debug)]
693 /// struct MyError;
694 ///
695 /// let err = at(MyError)
696 /// .at_str("loading config")
697 /// .at_str("initializing");
698 ///
699 /// let texts: Vec<_> = err.contexts()
700 /// .filter_map(|ctx| ctx.as_text())
701 /// .collect();
702 /// assert_eq!(texts, vec!["initializing", "loading config"]); // newest first
703 /// ```
704 #[inline]
705 pub fn contexts(&self) -> impl Iterator<Item = AtContextRef<'_>> {
706 self.trace.as_ref().into_iter().flat_map(|t| t.contexts())
707 }
708
709 /// Iterate over frames (location + contexts pairs), oldest first.
710 ///
711 /// This is the recommended way to traverse a trace. Each frame contains
712 /// a location (or None for skipped-frames marker) and its associated contexts.
713 ///
714 /// ## Example
715 ///
716 /// ```rust
717 /// use whereat::at;
718 ///
719 /// #[derive(Debug)]
720 /// struct MyError;
721 ///
722 /// let err = at(MyError)
723 /// .at_str("loading config")
724 /// .at();
725 ///
726 /// for frame in err.frames() {
727 /// if let Some(loc) = frame.location() {
728 /// println!("at {}:{}", loc.file(), loc.line());
729 /// }
730 /// for ctx in frame.contexts() {
731 /// println!(" - {}", ctx);
732 /// }
733 /// }
734 /// ```
735 #[inline]
736 pub fn frames(&self) -> impl Iterator<Item = AtFrame<'_>> {
737 self.trace.frames()
738 }
739
740 /// Get the number of frames in the trace.
741 #[inline]
742 pub fn frame_count(&self) -> usize {
743 self.trace.as_ref().map_or(0, |t| t.frame_count())
744 }
745
746 // ========================================================================
747 // Trace manipulation methods
748 // ========================================================================
749
750 /// Pop the most recent location and its contexts from the trace.
751 ///
752 /// Returns `None` if the trace is empty.
753 #[inline]
754 pub fn at_pop(&mut self) -> Option<AtFrameOwned> {
755 self.trace.as_mut()?.pop()
756 }
757
758 /// Push a segment (location + contexts) to the end of the trace.
759 #[inline]
760 pub fn at_push(&mut self, segment: AtFrameOwned) {
761 self.ensure_trace().push(segment);
762 }
763
764 /// Pop the oldest location and its contexts from the trace.
765 ///
766 /// Returns `None` if the trace is empty.
767 #[inline]
768 pub fn at_first_pop(&mut self) -> Option<AtFrameOwned> {
769 self.trace.as_mut()?.pop_first()
770 }
771
772 /// Insert a segment (location + contexts) at the beginning of the trace.
773 #[inline]
774 pub fn at_first_insert(&mut self, segment: AtFrameOwned) {
775 self.ensure_trace().push_first(segment);
776 }
777
778 /// Take the entire trace, leaving self with an empty trace.
779 #[inline]
780 pub fn take_trace(&mut self) -> Option<AtTrace> {
781 self.trace.take()
782 }
783
784 /// Set the trace, replacing any existing trace.
785 #[inline]
786 pub fn set_trace(&mut self, trace: AtTrace) {
787 self.trace.set(trace);
788 }
789
790 // ========================================================================
791 // Error conversion methods
792 // ========================================================================
793
794 /// Convert the error type while preserving the trace.
795 ///
796 /// ## Example
797 ///
798 /// ```rust
799 /// use whereat::{at, At};
800 ///
801 /// #[derive(Debug)]
802 /// struct Error1;
803 /// #[derive(Debug)]
804 /// struct Error2;
805 ///
806 /// impl From<Error1> for Error2 {
807 /// fn from(_: Error1) -> Self { Error2 }
808 /// }
809 ///
810 /// let err1: At<Error1> = at(Error1).at_str("context");
811 /// let err2: At<Error2> = err1.map_error(Error2::from);
812 /// assert_eq!(err2.frame_count(), 1);
813 /// ```
814 #[inline]
815 pub fn map_error<E2, F>(self, f: F) -> At<E2>
816 where
817 F: FnOnce(E) -> E2,
818 {
819 At {
820 error: f(self.error),
821 trace: self.trace,
822 }
823 }
824
825 /// Convert to an `AtTraceable` type, transferring the trace.
826 ///
827 /// The closure receives the inner error and should return an error type
828 /// that implements `AtTraceable`. The trace is then transferred to the
829 /// new error's embedded trace.
830 ///
831 /// ## Example
832 ///
833 /// ```rust
834 /// use whereat::{at, At, AtTrace, AtTraceable};
835 ///
836 /// #[derive(Debug)]
837 /// struct Inner;
838 ///
839 /// struct MyError {
840 /// trace: AtTrace,
841 /// }
842 ///
843 /// impl AtTraceable for MyError {
844 /// fn trace_mut(&mut self) -> &mut AtTrace { &mut self.trace }
845 /// fn trace(&self) -> Option<&AtTrace> { Some(&self.trace) }
846 /// fn fmt_message(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847 /// write!(f, "my error")
848 /// }
849 /// }
850 ///
851 /// let at_err: At<Inner> = at(Inner).at_str("context");
852 /// let my_err: MyError = at_err.into_traceable(|_| MyError { trace: AtTrace::new() });
853 /// ```
854 #[inline]
855 pub fn into_traceable<E2, F>(mut self, f: F) -> E2
856 where
857 F: FnOnce(E) -> E2,
858 E2: crate::trace::AtTraceable,
859 {
860 let mut new_err = f(self.error);
861 if let Some(trace) = self.trace.take() {
862 *new_err.trace_mut() = trace;
863 }
864 new_err
865 }
866}
867
868// ============================================================================
869// Debug impl for At<E>
870// ============================================================================
871
872impl<E: fmt::Debug> fmt::Debug for At<E> {
873 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
874 // Error header
875 writeln!(f, "Error: {:?}", self.error)?;
876
877 let Some(trace) = self.trace.as_ref() else {
878 return Ok(());
879 };
880
881 writeln!(f)?;
882
883 // Simple iteration: walk locations, show all contexts at each index
884 // None = skipped frame marker
885 for (i, loc_opt) in trace.iter().enumerate() {
886 match loc_opt {
887 Some(loc) => {
888 writeln!(f, " at {}:{}", loc.file(), loc.line())?;
889 for context in trace.contexts_at(i) {
890 match context {
891 AtContext::Text(msg) => writeln!(f, " ╰─ {}", msg)?,
892 AtContext::FunctionName(name) => writeln!(f, " ╰─ in {}", name)?,
893 AtContext::Debug(t) => writeln!(f, " ╰─ {:?}", &**t)?,
894 AtContext::Display(t) => writeln!(f, " ╰─ {}", &**t)?,
895 AtContext::Error(e) => writeln!(f, " ╰─ caused by: {}", e)?,
896 AtContext::Crate(_) => {} // Crate boundaries don't display in basic Debug
897 }
898 }
899 }
900 None => {
901 writeln!(f, " [...]")?;
902 }
903 }
904 }
905
906 Ok(())
907 }
908}
909
910// ============================================================================
911// Enhanced display with AtCrateInfo from trace
912// ============================================================================
913
914impl<E: fmt::Debug> At<E> {
915 /// Format the error with GitHub links using AtCrateInfo from the trace.
916 ///
917 /// When you use `at!()` or `.at_crate()`, the crate metadata is stored in
918 /// the trace. This method uses that metadata to generate clickable GitHub
919 /// links for each location.
920 ///
921 /// For cross-crate traces, each `at_crate()` call updates the repository
922 /// used for subsequent locations until another crate boundary is encountered.
923 ///
924 /// ## Example
925 ///
926 /// ```rust,ignore
927 /// // Requires define_at_crate_info!() setup
928 /// use whereat::{at, At};
929 ///
930 /// whereat::define_at_crate_info!();
931 ///
932 /// #[derive(Debug)]
933 /// struct MyError;
934 ///
935 /// let err = at!(MyError);
936 /// println!("{}", err.display_with_meta());
937 /// ```
938 #[inline]
939 pub fn display_with_meta(&self) -> impl fmt::Display + '_ {
940 DisplayWithMeta { traced: self }
941 }
942}
943
944/// Wrapper for displaying At<E> with AtCrateInfo enhancements.
945struct DisplayWithMeta<'a, E> {
946 traced: &'a At<E>,
947}
948
949impl<E: fmt::Debug> fmt::Display for DisplayWithMeta<'_, E> {
950 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951 // Error header
952 writeln!(f, "Error: {:?}", self.traced.error)?;
953
954 let Some(trace) = self.traced.trace.as_ref() else {
955 return Ok(());
956 };
957
958 // Use crate_info field first (set by at!() macro)
959 // at_crate() context entries can override this per-location
960 let initial_crate = trace.crate_info();
961
962 // Show crate info if available
963 if let Some(info) = initial_crate {
964 writeln!(f, " crate: {}", info.name())?;
965 }
966
967 writeln!(f)?;
968
969 // Cache GitHub base URL - rebuild when crate boundary changes
970 let mut link_template: Option<String> = initial_crate.and_then(build_link_base);
971
972 // Walk locations, updating GitHub base when we encounter crate boundaries
973 // None = skipped frame marker
974 for (i, loc_opt) in trace.iter().enumerate() {
975 // Check for crate boundary at this location - rebuild URL only when crate changes
976 for context in trace.contexts_at(i) {
977 if let AtContext::Crate(info) = context {
978 link_template = build_link_base(info);
979 }
980 }
981
982 match loc_opt {
983 Some(loc) => {
984 write_location_meta(f, loc, link_template.as_deref())?;
985
986 // Show non-crate contexts
987 for context in trace.contexts_at(i) {
988 match context {
989 AtContext::Text(msg) => writeln!(f, " ╰─ {}", msg)?,
990 AtContext::FunctionName(name) => writeln!(f, " ╰─ in {}", name)?,
991 AtContext::Debug(t) => writeln!(f, " ╰─ {:?}", &**t)?,
992 AtContext::Display(t) => writeln!(f, " ╰─ {}", &**t)?,
993 AtContext::Error(e) => writeln!(f, " ╰─ caused by: {}", e)?,
994 AtContext::Crate(_) => {} // Already handled above
995 }
996 }
997 }
998 None => {
999 writeln!(f, " [...]")?;
1000 }
1001 }
1002 }
1003
1004 Ok(())
1005 }
1006}
1007
1008/// Build URL base from crate info using the configured link format.
1009/// Returns the formatted URL base or None if repo/commit unavailable.
1010///
1011/// The format string can contain placeholders: `{repo}`, `{commit}`, `{path}`.
1012/// The `{file}` and `{line}` placeholders are handled by `write_location_meta`.
1013fn build_link_base(info: &AtCrateInfo) -> Option<String> {
1014 match (info.repo(), info.commit()) {
1015 (Some(repo), Some(commit)) => {
1016 let repo = repo.trim_end_matches('/');
1017 let path = info.crate_path().unwrap_or("");
1018 let format = info.link_format();
1019
1020 // Build the base URL by replacing {repo}, {commit}, {path}
1021 // Leave {file} and {line} for write_location_meta
1022 let mut result =
1023 String::with_capacity(format.len() + repo.len() + commit.len() + path.len());
1024 let mut chars = format.chars().peekable();
1025
1026 while let Some(c) = chars.next() {
1027 if c == '{' {
1028 // Look for placeholder
1029 let mut placeholder = String::new();
1030 while let Some(&next) = chars.peek() {
1031 if next == '}' {
1032 chars.next(); // consume '}'
1033 break;
1034 }
1035 placeholder.push(chars.next().unwrap());
1036 }
1037 match placeholder.as_str() {
1038 "repo" => result.push_str(repo),
1039 "commit" => result.push_str(commit),
1040 "path" => result.push_str(path),
1041 // Keep {file} and {line} as-is for later substitution
1042 other => {
1043 result.push('{');
1044 result.push_str(other);
1045 result.push('}');
1046 }
1047 }
1048 } else {
1049 result.push(c);
1050 }
1051 }
1052 Some(result)
1053 }
1054 _ => None,
1055 }
1056}
1057
1058/// Helper to write a location with optional repository link.
1059///
1060/// The `link_template` should have {repo}, {commit}, {path} already substituted,
1061/// but {file} and {line} still present as placeholders.
1062fn write_location_meta(
1063 f: &mut fmt::Formatter<'_>,
1064 loc: &'static Location<'static>,
1065 link_template: Option<&str>,
1066) -> fmt::Result {
1067 writeln!(f, " at {}:{}", loc.file(), loc.line())?;
1068 if let Some(template) = link_template {
1069 // Convert backslashes to forward slashes for Windows paths
1070 let file = loc.file().replace('\\', "/");
1071 let line = loc.line();
1072
1073 // Replace {file} and {line} placeholders
1074 let link = template
1075 .replace("{file}", &file)
1076 .replace("{line}", &line.to_string());
1077 writeln!(f, " {}", link)?;
1078 }
1079 Ok(())
1080}
1081
1082// ============================================================================
1083// Formatting methods for At<E>
1084// ============================================================================
1085
1086impl<E: fmt::Display> At<E> {
1087 /// Format with full trace (message + locations + all contexts).
1088 ///
1089 /// Returns a formatter that displays:
1090 /// - The error message (via `Display`)
1091 /// - All trace frame locations
1092 /// - All context strings at each location
1093 ///
1094 /// ## Example
1095 ///
1096 /// ```rust
1097 /// use whereat::{at, At};
1098 ///
1099 /// #[derive(Debug)]
1100 /// struct MyError(&'static str);
1101 ///
1102 /// impl std::fmt::Display for MyError {
1103 /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1104 /// write!(f, "{}", self.0)
1105 /// }
1106 /// }
1107 ///
1108 /// let err: At<MyError> = at(MyError("failed")).at_str("loading config");
1109 /// println!("{}", err.full_trace());
1110 /// // Output:
1111 /// // failed
1112 /// // at src/main.rs:10:1
1113 /// // loading config
1114 /// ```
1115 #[inline]
1116 pub fn full_trace(&self) -> impl fmt::Display + '_ {
1117 AtFullTraceDisplay { at: self }
1118 }
1119
1120 /// Format with trace locations only (message + locations, no context strings).
1121 ///
1122 /// Returns a formatter that displays:
1123 /// - The error message (via `Display`)
1124 /// - All trace frame locations
1125 /// - NO context strings (for compact output)
1126 ///
1127 /// ## Example
1128 ///
1129 /// ```rust
1130 /// use whereat::{at, At};
1131 ///
1132 /// #[derive(Debug)]
1133 /// struct MyError(&'static str);
1134 ///
1135 /// impl std::fmt::Display for MyError {
1136 /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1137 /// write!(f, "{}", self.0)
1138 /// }
1139 /// }
1140 ///
1141 /// let err: At<MyError> = at(MyError("failed")).at_str("loading config");
1142 /// println!("{}", err.last_error_trace());
1143 /// // Output:
1144 /// // failed
1145 /// // at src/main.rs:10:1
1146 /// ```
1147 #[inline]
1148 pub fn last_error_trace(&self) -> impl fmt::Display + '_ {
1149 AtLastErrorTraceDisplay { at: self }
1150 }
1151
1152 /// Format just the error message (no trace).
1153 ///
1154 /// Returns a formatter that only displays the error message via `Display`.
1155 /// Use this when you want to show the error without any trace information.
1156 ///
1157 /// This is equivalent to using the `Display` impl directly.
1158 #[inline]
1159 pub fn last_error(&self) -> impl fmt::Display + '_ {
1160 AtLastErrorDisplay { at: self }
1161 }
1162}
1163
1164/// Formatter that shows error message + full trace with all contexts.
1165struct AtFullTraceDisplay<'a, E> {
1166 at: &'a At<E>,
1167}
1168
1169impl<E: fmt::Display> fmt::Display for AtFullTraceDisplay<'_, E> {
1170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1171 // Show the error message
1172 write!(f, "{}", self.at.error)?;
1173
1174 // Show trace frames
1175 if let Some(trace) = self.at.trace.as_ref() {
1176 for frame in trace.frames() {
1177 if let Some(loc) = frame.location() {
1178 write!(f, "\n at {}:{}:{}", loc.file(), loc.line(), loc.column())?;
1179 } else {
1180 write!(f, "\n [...]")?;
1181 }
1182
1183 // Show contexts for this frame
1184 for ctx in frame.contexts() {
1185 if let Some(text) = ctx.as_text() {
1186 write!(f, "\n {}", text)?;
1187 } else if let Some(fn_name) = ctx.as_function_name() {
1188 write!(f, "\n in {}", fn_name)?;
1189 } else if let Some(err) = ctx.as_error() {
1190 write!(f, "\n caused by: {}", err)?;
1191 // Write nested error chain
1192 let mut source = err.source();
1193 let mut depth = 2;
1194 while let Some(src) = source {
1195 let indent = " ".repeat(depth);
1196 write!(f, "\n{}caused by: {}", indent, src)?;
1197 source = src.source();
1198 depth += 1;
1199 }
1200 } else {
1201 write!(f, "\n {}", ctx)?;
1202 }
1203 }
1204 }
1205 }
1206 Ok(())
1207 }
1208}
1209
1210/// Formatter that shows error message + trace locations only (no contexts).
1211struct AtLastErrorTraceDisplay<'a, E> {
1212 at: &'a At<E>,
1213}
1214
1215impl<E: fmt::Display> fmt::Display for AtLastErrorTraceDisplay<'_, E> {
1216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1217 // Show the error message
1218 write!(f, "{}", self.at.error)?;
1219
1220 // Show trace frames (locations only, no contexts)
1221 if let Some(trace) = self.at.trace.as_ref() {
1222 for frame in trace.frames() {
1223 if let Some(loc) = frame.location() {
1224 write!(f, "\n at {}:{}:{}", loc.file(), loc.line(), loc.column())?;
1225 } else {
1226 write!(f, "\n [...]")?;
1227 }
1228 }
1229 }
1230 Ok(())
1231 }
1232}
1233
1234/// Formatter that shows just the error message (no trace).
1235struct AtLastErrorDisplay<'a, E> {
1236 at: &'a At<E>,
1237}
1238
1239impl<E: fmt::Display> fmt::Display for AtLastErrorDisplay<'_, E> {
1240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241 write!(f, "{}", self.at.error)
1242 }
1243}
1244
1245// ============================================================================
1246// Display impl for At<E>
1247// ============================================================================
1248
1249impl<E: fmt::Display> fmt::Display for At<E> {
1250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1251 write!(f, "{}", self.error)
1252 }
1253}
1254
1255// ============================================================================
1256// Error impl for At<E>
1257// ============================================================================
1258
1259/// `At<E>` delegates [`source()`](core::error::Error::source) to `E::source()`.
1260///
1261/// Errors attached via [`.at_aside_error()`](At::at_aside_error) are **not** part of this
1262/// chain — they are diagnostic context stored in the trace, accessible via
1263/// [`.contexts()`](At::contexts) and [`.full_trace()`](At::full_trace).
1264///
1265/// See [`.at_aside_error()`](At::at_aside_error) for the rationale.
1266impl<E: core::error::Error> core::error::Error for At<E> {
1267 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
1268 self.error.source()
1269 }
1270}
1271
1272// ============================================================================
1273// From impl for At<E>
1274// ============================================================================
1275
1276impl<E> From<E> for At<E> {
1277 #[inline]
1278 fn from(error: E) -> Self {
1279 At::wrap(error)
1280 }
1281}
1282
1283// ============================================================================
1284// PartialEq impl for At<E> - compares only the error, not the trace
1285// ============================================================================
1286
1287impl<E: PartialEq> PartialEq for At<E> {
1288 /// Compare two `At<E>` errors by their inner error only.
1289 ///
1290 /// The trace is metadata about *where* the error was created, not *what*
1291 /// the error is. Two errors with the same `E` value are equal regardless
1292 /// of their traces.
1293 #[inline]
1294 fn eq(&self, other: &Self) -> bool {
1295 self.error == other.error
1296 }
1297}
1298
1299impl<E: Eq> Eq for At<E> {}
1300
1301impl<E: Hash> Hash for At<E> {
1302 /// Hash only the inner error, not the trace.
1303 ///
1304 /// Consistent with `PartialEq`: the trace is metadata, not identity.
1305 #[inline]
1306 fn hash<H: Hasher>(&self, state: &mut H) {
1307 self.error.hash(state);
1308 }
1309}
1310
1311// ============================================================================
1312// AsRef impl for At<E>
1313// ============================================================================
1314
1315impl<E> AsRef<E> for At<E> {
1316 #[inline]
1317 fn as_ref(&self) -> &E {
1318 &self.error
1319 }
1320}