Skip to main content

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}