yoshi_std/lib.rs
1/* yoshi/yoshi-std/src/lib.rs */
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![allow(dead_code)]
4#![warn(clippy::all)]
5#![warn(missing_docs)]
6#![warn(clippy::cargo)]
7#![warn(clippy::pedantic)]
8#![allow(clippy::too_many_lines)] // Allows for longer files, often necessary in comprehensive modules
9#![allow(clippy::result_large_err)] // Error handling framework intentionally uses large error types for rich context
10#![allow(clippy::enum_variant_names)] // For consistent naming of enum variants like IoError.
11#![allow(clippy::items_after_statements)] // Allows declaration of items after statements for better code flow in some contexts
12#![allow(clippy::module_name_repetitions)] // Allow for names like YoshiKind, YoContext.
13#![cfg_attr(not(feature = "std"), no_std)]
14//! **Brief:** Comprehensive error handling framework for robust Rust applications.
15//!
16//! Yoshi provides structured error types with rich contextual information, making it easier
17//! to debug, trace, and handle errors throughout your application. It offers flexible error
18//! categorization, context chaining, and optional backtrace capture while maintaining
19//! excellent performance characteristics.
20//!
21//! **Module Classification:** Performance-Critical\
22//! **Complexity Level:** Expert\
23//! **API Stability:** Stable
24//!
25//! ## Key Features
26//!
27//! **Structured Error Types**: Define precise error categories with relevant metadata
28//! rather than relying on string-based errors. Each error kind captures the specific
29//! information needed for that failure mode.
30//!
31//! **Rich Context**: Add diagnostic information, suggestions, and typed payloads
32//! as errors propagate through your application. Context is preserved without
33//! performance overhead.
34//!
35//! **Performance Focused**: Sub-microsecond error creation with O(1) context
36//! attachment. Backtrace capture is conditional and can be disabled in production.
37//!
38//! **`no_std` Compatible**: Full functionality available in `no_std` environments
39//! with automatic fallbacks for platform-specific features.
40//!
41//! ## Usage Patterns
42//!
43//! Yoshi works well for applications that need detailed error diagnostics and
44//! structured error handling. It's particularly useful when you want to:
45//!
46//! - Provide rich debugging information to developers
47//! - Maintain error context across call stacks
48//! - Categorize errors for different handling strategies
49//! - Include suggestions and metadata for error recovery
50//!
51//! For simpler error propagation needs, consider [`anyhow`]. For derive-based
52//! error definitions, [`thiserror`] remains an excellent choice and can be
53//! used alongside Yoshi.
54//!
55//! ## Core Types
56//!
57//! - [`Yoshi`]: The main error type providing structured error handling
58//! - [`YoshiKind`]: Error categories with type-specific fields
59//! - [`YoContext`]: Contextual information and metadata
60//! - [`HatchExt`]: Extension trait for `Result` types
61//! - [`YoshiLocation`]: Source code location capture
62//! - [`YoshiBacktrace`]: Performance-monitored backtrace wrapper
63//! - `NoStdIo`: I/O error type for `no_std` environments
64//! - [`Result`]: Type alias for `Result` with `Yoshi` as default error
65//! - [`error_instance_count()`]: Global counter for Yoshi error instances
66//!
67//! # Examples
68//!
69//! Basic error creation and context addition:
70//!
71//! ```
72//! use yoshi_std::{Yoshi, YoshiKind};
73//! # use std::io;
74//! # use std::io::ErrorKind;
75//! #
76//! # fn simulate_io_error() -> Result<(), io::Error> {
77//! # Err(io::Error::new(ErrorKind::PermissionDenied, "cannot access file"))
78//! # }
79//!
80//! fn load_config(path: &str) -> Result<String, Yoshi> {
81//! // Convert I/O errors to Yoshi errors with additional context
82//! simulate_io_error()
83//! .map_err(Yoshi::from)?;
84//!
85//! // Errors can be built up with context as they propagate
86//! Err(Yoshi::new(YoshiKind::NotFound {
87//! resource_type: "config file".into(),
88//! identifier: path.into(),
89//! search_locations: None,
90//! })
91//! .with_metadata("config_path", path)
92//! .with_suggestion("Ensure the configuration file exists and is readable")
93//! .context(format!("Failed to load configuration from {}", path)))
94//! }
95//!
96//! # fn main() {
97//! match load_config("/etc/app/config.json") {
98//! Ok(config) => println!("Loaded: {}", config),
99//! Err(error) => {
100//! eprintln!("Configuration error: {}", error);
101//! // Rich error output includes context, metadata, and suggestions
102//! }
103//! }
104//! # }
105//! ```
106//!
107//! Working with typed payloads and structured data:
108//!
109//! ```
110//! use yoshi_std::{Yoshi, YoshiKind};
111//!
112//! #[derive(Debug)]
113//! struct RequestId(String);
114//!
115//! fn process_request(id: &str) -> Result<(), Yoshi> {
116//! Err(Yoshi::new(YoshiKind::Timeout {
117//! operation: "database query".into(),
118//! duration: std::time::Duration::from_secs(30),
119//! expected_max: Some(std::time::Duration::from_secs(10)),
120//! })
121//! .with_shell(RequestId(id.to_string()))
122//! .with_metadata("user_id", "12345")
123//! .context("Request processing failed"))
124//! }
125//!
126//! # fn main() {
127//! if let Err(error) = process_request("req_001") {
128//! // Access structured data from the error
129//! if let Some(request_id) = error.shell::<RequestId>() {
130//! println!("Failed request: {:?}", request_id);
131//! }
132//!
133//! println!("Error details: {}", error);
134//! }
135//! # }
136//! ```
137//!
138//! [`anyhow`]: https://docs.rs/anyhow
139//! [`thiserror`]: https://docs.rs/thiserror
140//!
141// ~=####====A===r===c===M===o===o===n====S===t===u===d===i===o===s====X|0|$>
142//! + Structured error handling with context preservation [O(1) error creation, O(1) context attachment]
143//! - Type-safe error categorization with detailed diagnostic information [Memory-safe, Thread-safe]
144//! - Context chaining for complete error trace visibility [Stack-overflow protection, bounded depth]
145//! - Conditional backtrace capture with performance monitoring [Zero-cost when disabled]
146//! - Memory-efficient formatting with minimal allocations [Pre-allocated buffers, shared strings]
147// ~=####====A===r===c===M===o===o===n====S===t===u===d===i===o===s====X|0|$>
148// **GitHub:** [ArcMoon Studios](https://github.com/arcmoonstudios)
149// **Copyright:** (c) 2025 ArcMoon Studios
150// **License:** MIT OR Apache-2.0
151// **License File:** /LICENSE
152// **Contact:** LordXyn@proton.me
153// **Author:** Lord Xyn
154
155// Add serde helper functions for Arc<str> serialization
156#[cfg(feature = "serde")]
157mod serde_helpers {
158 use super::String;
159 use serde::{Deserialize, Deserializer, Serialize, Serializer};
160 use std::collections::HashMap;
161 use std::sync::Arc;
162
163 /// Serialize `Option<Arc<str>>` as `Option<String>`
164 #[allow(clippy::ref_option)]
165 pub fn serialize_arc_str<S>(value: &Option<Arc<str>>, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: Serializer,
168 {
169 value
170 .as_ref()
171 .map(std::convert::AsRef::as_ref)
172 .serialize(serializer)
173 }
174
175 /// Deserialize `Option<String>` as `Option<Arc<str>>`
176 pub fn deserialize_arc_str<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
177 where
178 D: Deserializer<'de>,
179 {
180 let opt_string: Option<String> = Option::deserialize(deserializer)?;
181 Ok(opt_string.map(|s| Arc::from(s.as_str())))
182 }
183
184 /// Serialize `HashMap<Arc<str>, Arc<str>>` as `HashMap<String, String>`
185 pub fn serialize_arc_str_map<S>(
186 value: &HashMap<Arc<str>, Arc<str>>,
187 serializer: S,
188 ) -> Result<S::Ok, S::Error>
189 where
190 S: Serializer,
191 {
192 let string_map: HashMap<&str, &str> = value
193 .iter()
194 .map(|(k, v)| (k.as_ref(), v.as_ref()))
195 .collect();
196 string_map.serialize(serializer)
197 }
198
199 /// Deserialize `HashMap<String, String>` as `HashMap<Arc<str>, Arc<str>>`
200 pub fn deserialize_arc_str_map<'de, D>(
201 deserializer: D,
202 ) -> Result<HashMap<Arc<str>, Arc<str>>, D::Error>
203 where
204 D: Deserializer<'de>,
205 {
206 let string_map: HashMap<String, String> = HashMap::deserialize(deserializer)?;
207 Ok(string_map
208 .into_iter()
209 .map(|(k, v)| (Arc::from(k.as_str()), Arc::from(v.as_str())))
210 .collect())
211 }
212}
213
214#[cfg(feature = "serde")]
215use serde_helpers::{
216 deserialize_arc_str, deserialize_arc_str_map, serialize_arc_str, serialize_arc_str_map,
217};
218
219// Unconditionally enable alloc crate for no_std builds using heap allocations
220#[cfg(not(feature = "std"))]
221extern crate alloc;
222
223// Unified imports for String, Vec, Box, Arc based on 'std' feature
224#[cfg(not(feature = "std"))]
225pub use alloc::{
226 boxed::Box,
227 string::{String, ToString},
228 sync::Arc,
229 vec::Vec,
230};
231#[cfg(feature = "std")]
232pub use std::{
233 boxed::Box,
234 string::{String, ToString},
235 sync::Arc,
236 vec::Vec,
237};
238
239use core::any::Any; // Import Any for error_generic_member_access and blanket From
240use core::error::Error; // Removed Request as it's unstable
241use core::fmt::{self, Display, Formatter};
242use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
243use core::time::Duration;
244
245// Additional imports for advanced features
246// Unified imports for HashMap based on 'std' feature
247#[cfg(not(feature = "std"))]
248use alloc::collections::BTreeMap as HashMap;
249#[cfg(feature = "std")]
250use std::collections::HashMap; // Using BTreeMap for no_std by default
251 // Unified imports for SystemTime and Thread based on 'std' feature
252#[cfg(feature = "std")]
253use std::{thread, time::SystemTime};
254#[cfg(not(feature = "std"))]
255/// Enhanced SystemTime for `no_std` environments with monotonic counter.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub struct SystemTime {
259 /// Monotonic timestamp counter for ordering events
260 timestamp: u64,
261}
262
263#[cfg(not(feature = "std"))]
264impl SystemTime {
265 /// Returns a `SystemTime` with monotonic ordering guarantees.
266 ///
267 /// While not wall-clock time, this provides ordering semantics
268 /// useful for debugging and event correlation in no_std environments.
269 pub fn now() -> Self {
270 static COUNTER: AtomicU64 = AtomicU64::new(0);
271 Self {
272 timestamp: COUNTER.fetch_add(1, Ordering::Relaxed),
273 }
274 }
275
276 /// Returns the internal timestamp for debugging purposes.
277 pub const fn timestamp(&self) -> u64 {
278 self.timestamp
279 }
280
281 /// Calculates duration since another SystemTime (in timestamp units).
282 pub const fn duration_since(&self, earlier: SystemTime) -> Option<u64> {
283 if self.timestamp >= earlier.timestamp {
284 Some(self.timestamp - earlier.timestamp)
285 } else {
286 None
287 }
288 }
289
290 /// Returns elapsed timestamp units since this SystemTime.
291 pub fn elapsed(&self) -> u64 {
292 Self::now().timestamp.saturating_sub(self.timestamp)
293 }
294}
295#[cfg(not(feature = "std"))]
296use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
297
298#[cfg(not(feature = "std"))]
299/// Enhanced ThreadId for `no_std` environments with unique identification.
300#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
301pub struct ThreadId {
302 /// Unique identifier for tracking execution contexts
303 id: u32,
304}
305
306#[cfg(not(feature = "std"))]
307impl ThreadId {
308 /// Returns a `ThreadId` with unique identification.
309 ///
310 /// In no_std environments, this provides unique identifiers
311 /// useful for correlating errors across different execution contexts.
312 pub fn current() -> Self {
313 static THREAD_COUNTER: AtomicU32 = AtomicU32::new(1);
314
315 // Use thread-local storage pattern with atomic fallback
316 #[cfg(all(target_has_atomic = "ptr", any(feature = "std", target_thread_local)))]
317 {
318 use core::cell::Cell;
319 thread_local! {
320 static THREAD_ID: Cell<Option<u32>> = const { Cell::new(None) };
321 }
322
323 THREAD_ID.with(|id| {
324 let current_id = id.get().unwrap_or_else(|| {
325 let new_id = THREAD_COUNTER.fetch_add(1, Ordering::Relaxed);
326 id.set(Some(new_id));
327 new_id
328 });
329
330 Self { id: current_id }
331 })
332 }
333 #[cfg(not(all(target_has_atomic = "ptr", any(feature = "std", target_thread_local))))]
334 {
335 // Fallback for platforms without atomic or thread_local support
336 Self {
337 id: THREAD_COUNTER.fetch_add(1, Ordering::Relaxed),
338 }
339 }
340 }
341
342 /// Returns the raw thread ID for debugging.
343 #[inline]
344 pub const fn as_u32(&self) -> u32 {
345 self.id
346 }
347
348 /// Creates a ThreadId from a raw ID (for testing/debugging).
349 pub const fn from_u32(id: u32) -> Self {
350 Self { id }
351 }
352}
353
354#[cfg(not(feature = "std"))]
355impl core::fmt::Display for ThreadId {
356 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
357 write!(f, "ThreadId({})", self.id)
358 }
359}
360
361// OnceLock is std-only, so it's only imported under std
362#[cfg(not(feature = "std"))]
363use core::cell::UnsafeCell;
364#[cfg(not(feature = "std"))]
365use core::sync::atomic::{AtomicBool, Ordering};
366#[cfg(feature = "std")]
367use std::sync::OnceLock;
368
369#[cfg(not(feature = "std"))]
370/// Thread-safe one-time initialization for `no_std` environments using atomics.
371pub struct OnceLock<T> {
372 initialized: AtomicBool,
373 data: UnsafeCell<Option<T>>,
374}
375
376#[cfg(not(feature = "std"))]
377unsafe impl<T: Send + Sync> Sync for OnceLock<T> {}
378#[cfg(not(feature = "std"))]
379unsafe impl<T: Send> Send for OnceLock<T> {}
380
381#[cfg(not(feature = "std"))]
382impl<T> OnceLock<T> {
383 /// Creates a new `OnceLock` for no_std environments.
384 pub const fn new() -> Self {
385 Self {
386 initialized: AtomicBool::new(false),
387 data: UnsafeCell::new(None),
388 }
389 }
390
391 /// Gets or initializes the value using atomic operations for thread safety.
392 pub fn get_or_init(&self, f: impl FnOnce() -> T) -> &T {
393 // Use compare_exchange for proper synchronization
394 if self
395 .initialized
396 .compare_exchange_weak(false, true, Ordering::AcqRel, Ordering::Acquire)
397 .is_ok()
398 {
399 let value = f();
400 unsafe {
401 let data_ptr = self.data.get();
402 *data_ptr = Some(value);
403 }
404 } else {
405 // Spin until initialization is complete
406 while !self.initialized.load(Ordering::Acquire) {
407 core::hint::spin_loop();
408 }
409 }
410
411 unsafe {
412 let data_ptr = self.data.get();
413 (*data_ptr).as_ref().unwrap_unchecked()
414 }
415 }
416
417 /// Gets the value if it has been initialized.
418 pub fn get(&self) -> Option<&T> {
419 if self.initialized.load(Ordering::Acquire) {
420 unsafe {
421 let data_ptr = self.data.get();
422 (*data_ptr).as_ref()
423 }
424 } else {
425 None
426 }
427 }
428}
429
430/// Enhanced wrapper for foreign errors with better context preservation
431#[derive(Debug)]
432struct ForeignErrorWrapper {
433 inner: Box<dyn Error + Send + Sync + 'static>,
434 context: String,
435 enhanced_message: String,
436}
437
438impl Display for ForeignErrorWrapper {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 if self.context.is_empty() {
441 write!(f, "{}", self.enhanced_message)
442 } else {
443 write!(f, "{}: {}", self.context, self.enhanced_message)
444 }
445 }
446}
447
448impl Error for ForeignErrorWrapper {
449 fn source(&self) -> Option<&(dyn Error + 'static)> {
450 Some(self.inner.as_ref())
451 }
452}
453
454/// High-performance buffer for error formatting with safe optimizations
455pub struct OptimizedFormatBuffer {
456 data: String,
457 reserved_capacity: usize,
458}
459
460impl OptimizedFormatBuffer {
461 const DEFAULT_CAPACITY: usize = 2048; // 2KB optimized default
462
463 /// Creates a new optimized format buffer with default capacity.
464 ///
465 /// Initializes a new `OptimizedFormatBuffer` with a default capacity of 4KB,
466 /// which is optimized for typical error formatting scenarios. The buffer
467 /// uses intelligent growth strategies to minimize memory allocations.
468 ///
469 /// # Returns
470 ///
471 /// A new `OptimizedFormatBuffer` instance with default capacity.
472 ///
473 /// # Examples
474 ///
475 /// ```
476 /// # use yoshi_std::OptimizedFormatBuffer;
477 /// let buffer = OptimizedFormatBuffer::new();
478 /// assert_eq!(buffer.as_str(), "");
479 /// ```
480 #[must_use]
481 pub fn new() -> Self {
482 Self {
483 data: String::with_capacity(Self::DEFAULT_CAPACITY),
484 reserved_capacity: Self::DEFAULT_CAPACITY,
485 }
486 }
487}
488
489impl Default for OptimizedFormatBuffer {
490 fn default() -> Self {
491 Self::new()
492 }
493}
494
495impl OptimizedFormatBuffer {
496 /// Creates a new optimized format buffer with specified capacity.
497 ///
498 /// Initializes a new `OptimizedFormatBuffer` with a custom initial capacity.
499 /// This is useful when you have an estimate of the final formatted size
500 /// and want to avoid reallocations during formatting operations.
501 ///
502 /// # Arguments
503 ///
504 /// * `capacity` - The initial capacity for the internal string buffer.
505 ///
506 /// # Returns
507 ///
508 /// A new `OptimizedFormatBuffer` instance with the specified capacity.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// # use yoshi_std::OptimizedFormatBuffer;
514 /// let buffer = OptimizedFormatBuffer::with_capacity(8192);
515 /// assert_eq!(buffer.as_str(), "");
516 /// ```
517 #[must_use]
518 pub fn with_capacity(capacity: usize) -> Self {
519 Self {
520 data: String::with_capacity(capacity),
521 reserved_capacity: capacity,
522 }
523 }
524
525 /// High-performance string appending with optimized growth strategy
526 pub fn append_optimized(&mut self, s: &str) {
527 let current_len = self.data.len();
528 let append_len = s.len();
529 let new_len = current_len + append_len;
530
531 // Optimized growth strategy: 1.5x growth with minimum thresholds
532 if new_len > self.data.capacity() {
533 let current_cap = self.data.capacity();
534 // Ensure minimum growth of at least reserved_capacity or 256 bytes for small buffers
535 let min_growth_needed = self.reserved_capacity.max(256);
536 let growth_target_1_5x = current_cap + (current_cap >> 1); // 1.5x growth
537 let new_capacity = growth_target_1_5x.max(new_len).max(min_growth_needed);
538
539 // Reserve exactly what we need to avoid over-allocation, but also ensure minimum
540 self.data.reserve(new_capacity - current_cap);
541 }
542
543 // Use efficient string concatenation, which is highly optimized by Rust
544 self.data.push_str(s);
545 }
546
547 /// Returns a string slice of the buffer's contents.
548 ///
549 /// This method provides read-only access to the formatted content within the buffer.
550 /// The returned string slice is guaranteed to be valid UTF-8 as all input is validated.
551 ///
552 /// # Returns
553 ///
554 /// A string slice containing the current buffer contents.
555 ///
556 /// # Examples
557 ///
558 /// ```rust
559 /// # use yoshi_std::OptimizedFormatBuffer;
560 /// let mut buffer = OptimizedFormatBuffer::new();
561 /// buffer.append_optimized("Hello, World!");
562 /// assert_eq!(buffer.as_str(), "Hello, World!");
563 /// ```
564 ///
565 /// # Performance
566 ///
567 /// This operation has O(1) time complexity and does not involve any allocations.
568 #[must_use]
569 pub fn as_str(&self) -> &str {
570 &self.data
571 }
572
573 /// Clears the buffer contents while preserving the allocated capacity.
574 ///
575 /// This method efficiently removes all content from the buffer without
576 /// deallocating the underlying storage. This allows for optimal memory reuse
577 /// when the buffer will be used again with similar content sizes.
578 ///
579 /// # Examples
580 ///
581 /// ```rust
582 /// # use yoshi_std::OptimizedFormatBuffer;
583 /// let mut buffer = OptimizedFormatBuffer::new();
584 /// buffer.append_optimized("Hello, World!");
585 /// assert_eq!(buffer.as_str().len(), 13);
586 ///
587 /// buffer.clear();
588 /// assert_eq!(buffer.as_str().len(), 0);
589 /// assert!(buffer.as_str().is_empty());
590 /// ```
591 ///
592 /// # Performance
593 ///
594 /// This operation has O(1) time complexity and preserves allocated capacity
595 /// for optimal memory reuse patterns.
596 pub fn clear(&mut self) {
597 self.data.clear();
598 }
599
600 /// Optimized formatting for multiple string fragments
601 pub fn append_multiple(&mut self, fragments: &[&str]) {
602 let total_len: usize = fragments.iter().map(|s| s.len()).sum();
603 let new_len = self.data.len() + total_len;
604
605 if new_len > self.data.capacity() {
606 let new_capacity = (new_len * 2)
607 .next_power_of_two()
608 .max(self.reserved_capacity);
609 self.data.reserve_exact(new_capacity - self.data.capacity());
610 }
611
612 for fragment in fragments {
613 self.data.push_str(fragment);
614 }
615 }
616}
617
618/// Comprehensive error recovery strategies
619#[derive(Debug, Clone)]
620pub enum ErrorRecoveryStrategy {
621 /// Retry with exponential backoff
622 ExponentialBackoff {
623 /// Initial delay before the first retry attempt
624 initial_delay: Duration,
625 /// Maximum number of retry attempts before giving up
626 max_retries: u32,
627 /// Multiplier for exponential backoff calculation (e.g., 2.0 for doubling)
628 backoff_multiplier: f64,
629 },
630 /// Retry with fixed intervals
631 FixedInterval {
632 /// Fixed time interval between retry attempts
633 interval: Duration,
634 /// Maximum number of retry attempts before giving up
635 max_retries: u32,
636 },
637 /// Fallback to alternative approach
638 Fallback {
639 /// Human-readable description of the fallback strategy
640 description: String,
641 },
642 /// Circuit breaker pattern
643 CircuitBreaker {
644 /// Number of consecutive failures before opening the circuit
645 failure_threshold: u32,
646 /// Timeout duration before attempting to close the circuit
647 recovery_timeout: Duration,
648 },
649 /// No recovery possible
650 NonRecoverable,
651}
652
653/// Detailed context analysis results
654#[derive(Debug, Default)]
655pub struct ContextAnalysis {
656 /// Total number of context objects attached to the error
657 pub total_contexts: usize,
658 /// Maximum depth of nested context information
659 pub context_depth: usize,
660 /// Whether the error includes user-facing suggestions
661 pub has_suggestions: bool,
662 /// Whether source code location information is available
663 pub has_location_info: bool,
664 /// Number of metadata key-value pairs attached
665 pub metadata_entries: usize,
666 /// Number of typed shell objects attached
667 pub typed_payloads: usize,
668 /// Priority level of the primary context (0-255)
669 pub primary_context_priority: u8,
670}
671
672/// Performance-optimized Result alias with mathematical precision guarantees.
673///
674/// This type alias simplifies the use of `Result` where the error type is
675/// fixed to [`Yoshi`]. It automatically adapts between `std::result::Result`
676/// and `core::result::Result` based on the enabled features.
677///
678/// # Examples
679///
680/// ```
681/// use yoshi_std::{Result, Yoshi, YoshiKind};
682///
683/// fn divide(a: f64, b: f64) -> Result<f64> {
684/// if b == 0.0 {
685/// return Err(Yoshi::new(YoshiKind::Validation {
686/// field: "divisor".into(),
687/// message: "Division by zero is not allowed".into(),
688/// expected: Some("non-zero".into()),
689/// actual: Some("0.0".into()),
690/// }));
691/// }
692/// Ok(a / b)
693/// }
694///
695/// let result = divide(10.0, 2.0);
696/// assert!(result.is_ok());
697/// assert_eq!(result.unwrap(), 5.0);
698///
699/// let error_result = divide(10.0, 0.0);
700/// assert!(error_result.is_err());
701/// ```
702#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
703#[cfg(feature = "std")]
704pub type Result<T, E = Yoshi> = std::result::Result<T, E>;
705#[cfg(not(feature = "std"))]
706/// Performance-optimized Result alias for `no_std` builds.
707///
708/// This type alias simplifies the use of `Result` where the error type is
709/// fixed to [`Yoshi`]. It automatically adapts between `std::result::Result`
710/// and `core::result::Result` based on the enabled features.
711///
712/// # Examples
713///
714/// ```
715/// use yoshi_std::{Result, Yoshi, YoshiKind, NoStdIo};
716///
717/// fn check_value(value: i32) -> Result<i32> {
718/// if value < 0 {
719/// return Err(Yoshi::new(YoshiKind::Validation {
720/// field: "value".into(),
721/// message: "Value cannot be negative".into(),
722/// expected: Some("non-negative".into()),
723/// actual: Some(value.to_string().into()),
724/// }));
725/// }
726/// Ok(value)
727/// }
728///
729/// let result = check_value(5);
730/// assert!(result.is_ok());
731/// assert_eq!(result.unwrap(), 5);
732///
733/// let error_result = check_value(-1);
734/// assert!(error_result.is_err());
735/// ```
736pub type Result<T, E = Yoshi> = core::result::Result<T, E>;
737
738/// Ergonomic type alias for `Result<T, Yoshi>` with thematic naming.
739///
740/// This type alias provides expressive naming that aligns with the Yoshi metaphorical
741/// framework while maintaining zero-cost abstraction guarantees. It automatically
742/// adapts between `std::result::Result` and `core::result::Result` based on features.
743///
744/// # Performance Characteristics
745///
746/// - **Time Complexity**: O(1) for all operations (zero-cost abstraction)
747/// - **Space Complexity**: Identical to `Result<T, Yoshi>` (no overhead)
748/// - **Memory Layout**: Exact same representation as standard `Result`
749///
750/// # Examples
751///
752/// ```rust
753/// use yoshi_std::{Hatch, Yoshi, YoshiKind};
754///
755/// fn load_config() -> Hatch<String> {
756/// Ok("configuration data".into())
757/// }
758///
759/// fn process_data() -> Hatch<u32> {
760/// Err(Yoshi::new(YoshiKind::Internal {
761/// message: "processing failed".into(),
762/// source: None,
763/// component: None,
764/// }))
765/// }
766/// ```
767pub type Hatch<T> = Result<T, Yoshi>;
768
769/// Global error instance counter for debugging and performance monitoring.
770///
771/// This atomic counter tracks the total number of `Yoshi` error instances
772/// that have been created since the application started. It's primarily
773/// used for performance monitoring and diagnostic purposes.
774static ERROR_INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(0);
775
776/// Global string interning pool for optimal memory reuse
777static STRING_INTERN_POOL: OnceLock<StringInternPool> = OnceLock::new();
778
779/// Checks if running in production mode for security sanitization
780#[inline]
781fn is_production_mode() -> bool {
782 #[cfg(feature = "std")]
783 {
784 std::env::var("YOSHI_PRODUCTION_MODE")
785 .map(|v| v == "1" || v.to_lowercase() == "true")
786 .unwrap_or(false)
787 }
788 #[cfg(not(feature = "std"))]
789 {
790 false // Default to development mode in no_std
791 }
792}
793
794/// Sanitizes error messages to remove potentially sensitive information in production
795fn sanitize_error_message(msg: &str) -> String {
796 // Define constants first before any statements
797 const MAX_MESSAGE_LENGTH: usize = 256;
798
799 let mut sanitized = msg.to_string();
800
801 // Simple string replacement for common sensitive patterns
802 let lower_msg = msg.to_lowercase();
803 if lower_msg.contains("password") {
804 sanitized = sanitized.replace("password", "password=[REDACTED]");
805 }
806 if lower_msg.contains("token") {
807 sanitized = sanitized.replace("token", "token=[REDACTED]");
808 }
809 if lower_msg.contains("key") {
810 sanitized = sanitized.replace("key", "key=[REDACTED]");
811 }
812
813 // Truncate very long messages that might contain sensitive data dumps
814 if sanitized.len() > MAX_MESSAGE_LENGTH {
815 sanitized.truncate(MAX_MESSAGE_LENGTH);
816 sanitized.push_str("... [truncated]");
817 }
818
819 sanitized
820}
821
822/// High-performance string interning with autonomous memory management and lock-free fast paths
823struct StringInternPool {
824 #[cfg(feature = "std")]
825 pool: std::sync::RwLock<std::collections::HashMap<String, Arc<str>>>,
826 #[cfg(not(feature = "std"))]
827 pool: alloc::collections::BTreeMap<String, Arc<str>>,
828 hits: AtomicUsize,
829 misses: AtomicUsize,
830 cache_size: AtomicUsize,
831}
832
833impl StringInternPool {
834 fn new() -> Self {
835 Self {
836 #[cfg(feature = "std")]
837 pool: std::sync::RwLock::new(std::collections::HashMap::with_capacity(128)),
838 #[cfg(not(feature = "std"))]
839 pool: alloc::collections::BTreeMap::new(),
840 hits: AtomicUsize::new(0),
841 misses: AtomicUsize::new(0),
842 cache_size: AtomicUsize::new(0),
843 }
844 }
845
846 /// Clears the interning pool to prevent memory leaks in long-running applications
847 #[cfg(feature = "std")]
848 pub fn clear_pool(&self) {
849 if let Ok(mut pool) = self.pool.write() {
850 pool.clear();
851 self.cache_size.store(0, Ordering::Release);
852 }
853 }
854
855 fn intern(&self, s: impl Into<String>) -> Arc<str> {
856 let string = s.into();
857
858 // Early exit for empty strings
859 if string.is_empty() {
860 return Arc::from("");
861 }
862
863 #[cfg(feature = "std")]
864 {
865 // Fast path: check if already interned with non-blocking try_read for performance
866 if let Ok(pool) = self.pool.try_read() {
867 if let Some(interned) = pool.get(&string) {
868 self.hits.fetch_add(1, Ordering::Relaxed);
869 return interned.clone();
870 }
871 }
872
873 // Cache size check before expensive write lock
874 const MAX_CACHE_SIZE: usize = 512;
875 let current_size = self.cache_size.load(Ordering::Relaxed);
876 if current_size >= MAX_CACHE_SIZE {
877 // Skip interning for large caches to prevent memory bloat
878 self.misses.fetch_add(1, Ordering::Relaxed);
879 return string.into();
880 }
881
882 // Slow path: intern new string (acquire write lock)
883 let mut pool = self
884 .pool
885 .write()
886 .unwrap_or_else(std::sync::PoisonError::into_inner);
887
888 // Double-check pattern (after acquiring write lock, for race conditions during read)
889 if let Some(interned) = pool.get(&string) {
890 self.hits.fetch_add(1, Ordering::Relaxed);
891 return interned.clone();
892 }
893
894 let current_pool_size = pool.len();
895 if current_pool_size < MAX_CACHE_SIZE {
896 let arc_str: Arc<str> = string.as_str().into();
897 pool.insert(string, arc_str.clone());
898 self.cache_size
899 .store(current_pool_size + 1, Ordering::Release);
900 self.misses.fetch_add(1, Ordering::Relaxed);
901 arc_str
902 } else {
903 // Cache is full, return without interning
904 self.cache_size.store(current_pool_size, Ordering::Release);
905 self.misses.fetch_add(1, Ordering::Relaxed);
906 string.into()
907 }
908 }
909
910 #[cfg(not(feature = "std"))]
911 {
912 // High-performance lock-free string interning using separate chaining with explicit capacity management
913 use core::ptr;
914 use core::sync::atomic::AtomicPtr;
915
916 // Fixed-size lock-free cache with atomic slots (larger for fewer collisions)
917 const CACHE_SLOTS: usize = 256; // Power of 2 for efficient modulo
918 static CACHE: [AtomicPtr<CacheEntry>; CACHE_SLOTS] =
919 [const { AtomicPtr::new(ptr::null_mut()) }; CACHE_SLOTS];
920
921 // Global maximum number of interned strings to prevent unbounded memory growth in no_std
922 const MAX_GLOBAL_CACHE_SIZE: usize = 512;
923
924 #[repr(C)]
925 struct CacheEntry {
926 hash: u64,
927 arc_str: Arc<str>,
928 next: AtomicPtr<CacheEntry>,
929 }
930
931 // Fast hash function for cache slot selection (FNV-1a)
932 #[inline(always)] // Ensure inlining for performance-critical path
933 fn fast_hash(s: &str) -> u64 {
934 let mut hash = 0xcbf29ce484222325u64; // FNV-1a offset basis
935 for byte in s.bytes() {
936 hash ^= byte as u64;
937 hash = hash.wrapping_mul(0x100000001b3u64); // FNV-1a prime
938 }
939 hash
940 }
941
942 let hash = fast_hash(&string);
943 let slot_index = (hash as usize) & (CACHE_SLOTS - 1); // Efficient modulo for power of 2
944
945 // Lock-free search in the cache slot's linked list
946 let mut current = CACHE[slot_index].load(Ordering::Acquire);
947 while !current.is_null() {
948 unsafe {
949 let entry = &*current;
950 if entry.hash == hash && entry.arc_str.as_ref() == string {
951 self.hits.fetch_add(1, Ordering::Relaxed);
952 return entry.arc_str.clone();
953 }
954 current = entry.next.load(Ordering::Acquire);
955 }
956 }
957
958 // Cache miss: attempt to increment global cache size *before* allocation
959 let new_cache_size = self.cache_size.fetch_add(1, Ordering::Relaxed) + 1;
960 if new_cache_size > MAX_GLOBAL_CACHE_SIZE {
961 // If over capacity, decrement counter (to prevent false overflow) and return original string
962 self.cache_size.fetch_sub(1, Ordering::Relaxed); // Correct the increment
963 self.misses.fetch_add(1, Ordering::Relaxed);
964 return string.into(); // Return uninterned string
965 }
966
967 let arc_str: Arc<str> = string.into(); // Allocate string
968 let new_entry = Box::into_raw(Box::new(CacheEntry {
969 hash,
970 arc_str: arc_str.clone(),
971 next: AtomicPtr::new(ptr::null_mut()),
972 }));
973
974 // Atomic compare-and-swap insertion at head of linked list
975 let mut head = CACHE[slot_index].load(Ordering::Acquire);
976 loop {
977 unsafe {
978 (*new_entry).next.store(head, Ordering::Relaxed);
979 }
980
981 match CACHE[slot_index].compare_exchange_weak(
982 head,
983 new_entry,
984 Ordering::Release,
985 Ordering::Acquire,
986 ) {
987 Ok(_) => {
988 // Successfully inserted new entry
989 self.misses.fetch_add(1, Ordering::Relaxed);
990 return arc_str;
991 }
992 Err(current_head) => {
993 // Another thread modified the head, retry with new head
994 head = current_head;
995
996 // Double-check if another thread inserted our string
997 let mut search_current = head;
998 while !search_current.is_null() {
999 unsafe {
1000 let entry = &*search_current;
1001 if entry.hash == hash && entry.arc_str.as_ref() == string {
1002 // Another thread inserted our string, clean up and return
1003 let _ = unsafe { Box::from_raw(new_entry) }; // Clean up unused entry
1004 self.hits.fetch_add(1, Ordering::Relaxed);
1005 self.cache_size.fetch_sub(1, Ordering::Relaxed); // Correct the size
1006 return entry.arc_str.clone();
1007 }
1008 search_current = entry.next.load(Ordering::Acquire);
1009 }
1010 }
1011 // Continue loop to retry insertion
1012 }
1013 }
1014 }
1015 }
1016 }
1017
1018 /// Returns (hits, misses) for performance monitoring
1019 #[inline]
1020 pub fn stats(&self) -> (usize, usize) {
1021 (
1022 self.hits.load(Ordering::Relaxed),
1023 self.misses.load(Ordering::Relaxed),
1024 )
1025 }
1026
1027 /// Returns current cache size for autonomous memory monitoring
1028 #[inline]
1029 pub fn cache_size(&self) -> usize {
1030 self.cache_size.load(Ordering::Acquire)
1031 }
1032}
1033
1034/// Optimized string interning function
1035#[inline]
1036pub fn intern_string(s: impl Into<String>) -> Arc<str> {
1037 STRING_INTERN_POOL
1038 .get_or_init(StringInternPool::new)
1039 .intern(s)
1040}
1041
1042/// Gets the current number of Yoshi error instances created.
1043///
1044/// This function provides a way to inspect the cumulative count of `Yoshi`
1045/// error objects instantiated. It can be useful for profiling, detecting
1046/// excessive error creation, or understanding error patterns in an
1047/// application.
1048///
1049/// # Returns
1050///
1051/// The total number of `Yoshi` error instances created as a `u64`.
1052///
1053/// # Examples
1054///
1055/// ```
1056/// use yoshi_std::{Yoshi, YoshiKind, error_instance_count};
1057///
1058/// let initial_count = error_instance_count();
1059/// let _err1 = Yoshi::new(YoshiKind::Internal {
1060/// message: "simulated error 1".into(),
1061/// source: None,
1062/// component: None,
1063/// });
1064/// let _err2 = Yoshi::new(YoshiKind::Internal {
1065/// message: "simulated error 2".into(),
1066/// source: None,
1067/// component: None,
1068/// });
1069///
1070/// assert_eq!(error_instance_count(), initial_count + 2);
1071/// ```
1072pub fn error_instance_count() -> u32 {
1073 ERROR_INSTANCE_COUNTER.load(Ordering::Relaxed)
1074}
1075
1076/// Resets the global error instance counter.
1077///
1078/// This function is intended primarily for use in test environments
1079/// to ensure test isolation and predictable counter values.
1080/// It should **not** be used in production code.
1081#[cfg(test)]
1082#[inline]
1083pub fn reset_error_instance_counter() {
1084 ERROR_INSTANCE_COUNTER.store(0, Ordering::Relaxed);
1085}
1086
1087//--------------------------------------------------------------------------------------------------
1088// Enhanced NoStdIo with performance optimization
1089//--------------------------------------------------------------------------------------------------
1090
1091#[cfg(not(feature = "std"))]
1092/// Structured error kinds for better type safety in no_std I/O operations.
1093#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1094pub enum NoStdIoKind {
1095 /// A file or directory was not found.
1096 NotFound,
1097 /// Permission was denied for the operation.
1098 PermissionDenied,
1099 /// A network connection was refused.
1100 ConnectionRefused,
1101 /// An operation timed out.
1102 TimedOut,
1103 /// A generic I/O error occurred.
1104 Generic,
1105 /// Other error types not covered by specific variants.
1106 Other,
1107}
1108
1109#[cfg(not(feature = "std"))]
1110impl NoStdIoKind {
1111 /// Returns a human-readable description of the error kind.
1112 pub const fn as_str(&self) -> &'static str {
1113 match self {
1114 Self::NotFound => "not found",
1115 Self::PermissionDenied => "permission denied",
1116 Self::ConnectionRefused => "connection refused",
1117 Self::TimedOut => "timed out",
1118 Self::Generic => "I/O error",
1119 Self::Other => "other error",
1120 }
1121 }
1122
1123 /// Returns whether this error kind typically indicates a transient condition.
1124 pub const fn is_transient(&self) -> bool {
1125 matches!(
1126 self,
1127 Self::ConnectionRefused | Self::TimedOut | Self::Generic
1128 )
1129 }
1130
1131 /// Returns a severity level for this error kind (0-100).
1132 pub const fn severity(&self) -> u8 {
1133 match self {
1134 Self::NotFound => 30,
1135 Self::PermissionDenied => 50,
1136 Self::ConnectionRefused => 40,
1137 Self::TimedOut => 35,
1138 Self::Generic => 45,
1139 Self::Other => 40,
1140 }
1141 }
1142}
1143
1144#[cfg(not(feature = "std"))]
1145impl core::fmt::Display for NoStdIoKind {
1146 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1147 f.write_str(self.as_str())
1148 }
1149}
1150
1151/// High-performance minimal wrapper for I/O errors in `no_std` contexts.
1152///
1153/// This enum provides a compact representation for common I/O errors
1154/// when the standard library's `std::io::Error` is not available.
1155/// It uses `Arc<str>` for message storage to minimize allocations
1156/// when messages are repeated or shared.
1157#[cfg(not(feature = "std"))]
1158#[derive(Debug, Clone)]
1159#[cfg_attr(docsrs, doc(cfg(not(feature = "std"))))]
1160pub enum NoStdIo {
1161 /// Generic I/O error with optimized string storage.
1162 GenericIo(Arc<str>),
1163 /// Indicates that a file or directory was not found.
1164 NotFound,
1165 /// Indicates that permission was denied for an operation.
1166 PermissionDenied,
1167 /// Indicates that a network connection was refused.
1168 ConnectionRefused,
1169 /// Indicates that an operation timed out.
1170 TimedOut,
1171 /// Other I/O errors, with a custom message.
1172 Other(Arc<str>),
1173}
1174
1175#[cfg(not(feature = "std"))]
1176impl NoStdIo {
1177 /// Creates a new I/O error with comprehensive categorization.
1178 ///
1179 /// This constructor attempts to categorize the error message into specific
1180 /// variants using pattern matching on common error strings, enabling
1181 /// better programmatic error handling even in no_std environments.
1182 ///
1183 /// # Arguments
1184 ///
1185 /// * `message` - A message describing the I/O error. This can be any type
1186 /// that converts into a `String`.
1187 ///
1188 /// # Returns
1189 ///
1190 /// A new `NoStdIo` error instance.
1191 ///
1192 /// # Examples
1193 ///
1194 /// ```
1195 /// # use yoshi_std::NoStdIo;
1196 /// let err1 = NoStdIo::new("file not found");
1197 /// assert!(matches!(err1, NoStdIo::NotFound));
1198 ///
1199 /// let err2 = NoStdIo::new("disk full");
1200 /// assert!(matches!(err2, NoStdIo::Other(_)));
1201 /// ```
1202 pub fn new(message: impl Into<String>) -> Self {
1203 let msg = message.into();
1204 let lower_msg = msg.to_lowercase();
1205
1206 // Comprehensive pattern matching for better error categorization
1207 match lower_msg.as_str() {
1208 // File/resource not found patterns
1209 s if s.contains("not found")
1210 || s.contains("no such file")
1211 || s.contains("enoent")
1212 || s.contains("file does not exist") =>
1213 {
1214 Self::NotFound
1215 }
1216
1217 // Permission/access denied patterns
1218 s if s.contains("permission denied")
1219 || s.contains("access denied")
1220 || s.contains("access is denied")
1221 || s.contains("eacces")
1222 || s.contains("unauthorized")
1223 || s.contains("forbidden") =>
1224 {
1225 Self::PermissionDenied
1226 }
1227
1228 // Network connection patterns
1229 s if s.contains("connection refused")
1230 || s.contains("econnrefused")
1231 || s.contains("no route to host")
1232 || s.contains("network unreachable") =>
1233 {
1234 Self::ConnectionRefused
1235 }
1236
1237 // Timeout patterns
1238 s if s.contains("timed out")
1239 || s.contains("timeout")
1240 || s.contains("etimedout")
1241 || s.contains("operation timeout") =>
1242 {
1243 Self::TimedOut
1244 }
1245
1246 // Generic I/O patterns
1247 s if s.contains("i/o error")
1248 || s.contains("io error")
1249 || s.contains("input/output error") =>
1250 {
1251 Self::GenericIo(msg.into())
1252 }
1253
1254 // Catch-all for unrecognized patterns
1255 _ => Self::Other(msg.into()),
1256 }
1257 }
1258
1259 /// Creates a new I/O error from an error code and message.
1260 ///
1261 /// This method provides more precise error categorization when
1262 /// both an error code and message are available.
1263 pub fn from_code_and_message(code: i32, message: impl Into<String>) -> Self {
1264 match code {
1265 2 | -2 => Self::NotFound, // ENOENT
1266 13 | -13 => Self::PermissionDenied, // EACCES
1267 111 | -111 => Self::ConnectionRefused, // ECONNREFUSED
1268 110 | -110 => Self::TimedOut, // ETIMEDOUT
1269 5 | -5 => Self::GenericIo(message.into().into()), // EIO
1270 _ => Self::Other(message.into().into()),
1271 }
1272 }
1273
1274 /// Creates a typed I/O error for specific common scenarios.
1275 pub fn typed_error(error_type: NoStdIoKind, message: impl Into<String>) -> Self {
1276 match error_type {
1277 NoStdIoKind::NotFound => Self::NotFound,
1278 NoStdIoKind::PermissionDenied => Self::PermissionDenied,
1279 NoStdIoKind::ConnectionRefused => Self::ConnectionRefused,
1280 NoStdIoKind::TimedOut => Self::TimedOut,
1281 NoStdIoKind::Generic => Self::GenericIo(message.into().into()),
1282 NoStdIoKind::Other => Self::Other(message.into().into()),
1283 }
1284 }
1285}
1286
1287#[cfg(not(feature = "std"))]
1288impl Display for NoStdIo {
1289 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
1290 match self {
1291 Self::GenericIo(s) => write!(f, "I/O error (no_std): {s}"),
1292 Self::NotFound => f.write_str("I/O error (no_std): not found"),
1293 Self::PermissionDenied => f.write_str("I/O error (no_std): permission denied"),
1294 Self::ConnectionRefused => f.write_str("I/O error (no_std): connection refused"),
1295 Self::TimedOut => f.write_str("I/O error (no_std): timed out"),
1296 Self::Other(s) => write!(f, "I/O error (no_std): {s}"),
1297 }
1298 }
1299}
1300
1301#[cfg(not(feature = "std"))]
1302impl Error for NoStdIo {}
1303
1304//--------------------------------------------------------------------------------------------------
1305// Enhanced YoshiKind with performance optimization
1306//--------------------------------------------------------------------------------------------------
1307
1308/// High‑level categories for recoverable failures with performance optimizations.
1309///
1310/// This enum represents the fundamental classification of an error within the
1311/// `Yoshi` framework. Each variant provides specific fields relevant to its
1312/// error category, enabling rich, structured error reporting and programmatic
1313/// error handling.
1314#[derive(Debug)]
1315#[non_exhaustive]
1316pub enum YoshiKind {
1317 /// Standard I/O failure with optimized error representation.
1318 /// /// This variant wraps `std::io::Error` when the `std` feature is enabled,
1319 /// or `NoStdIo` for `no_std` environments.
1320 #[cfg(feature = "std")]
1321 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
1322 Io(std::io::Error),
1323 /// I/O failure in `no_std` with enhanced error categorization.
1324 ///
1325 /// This variant wraps [`NoStdIo`] when the `std` feature is not enabled.
1326 #[cfg(not(feature = "std"))]
1327 #[cfg_attr(docsrs, doc(cfg(not(feature = "std"))))]
1328 Io(NoStdIo),
1329 /// Network-related error with connection and protocol context.
1330 ///
1331 /// This variant represents errors that occur during network operations,
1332 /// including connectivity issues, protocol errors, and communication failures.
1333 ///
1334 /// # Fields
1335 ///
1336 /// * `message` - A human-readable description of the network error
1337 /// * `source` - An optional nested [`Yoshi`] error that caused this network issue
1338 /// * `error_code` - An optional numeric error code from the underlying network layer
1339 ///
1340 /// # Examples
1341 ///
1342 /// ```
1343 /// # use std::sync::Arc;
1344 /// # use yoshi_std::{Yoshi, YoshiKind};
1345 /// let network_error = YoshiKind::Network {
1346 /// message: Arc::from("Connection refused"),
1347 /// source: None,
1348 /// error_code: Some(111),
1349 /// };
1350 /// ```
1351 Network {
1352 /// A human-readable description of the network error.
1353 message: Arc<str>,
1354 /// An optional nested [`Yoshi`] error that caused this network issue.
1355 source: Option<Box<Yoshi>>,
1356 /// An optional network-specific error code (e.g., HTTP status code).
1357 error_code: Option<u32>,
1358 },
1359 /// Configuration error with enhanced diagnostics.
1360 ///
1361 /// Fields:
1362 /// - `message`: A human-readable description of the configuration error.
1363 /// - `source`: An optional nested `Yoshi` error that caused this configuration issue.
1364 /// - `config_path`: An optional path to the configuration file or source.
1365 Config {
1366 /// A human-readable description of the configuration error.
1367 message: Arc<str>,
1368 /// An optional nested [`Yoshi`] error that caused this configuration issue.
1369 source: Option<Box<Yoshi>>,
1370 /// An optional path to the configuration file or source.
1371 config_path: Option<Arc<str>>,
1372 },
1373 /// Data validation failure with field-level precision.
1374 ///
1375 /// Fields:
1376 /// - `field`: The name of the field that failed validation.
1377 /// - `message`: A description of why the validation failed.
1378 /// - `expected`: An optional description of the expected value or format.
1379 /// - `actual`: An optional string representation of the actual value received.
1380 Validation {
1381 /// The name of the field that failed validation.
1382 field: Arc<str>,
1383 /// A description of why the validation failed.
1384 message: Arc<str>,
1385 /// An optional description of the expected value or format.
1386 expected: Option<Arc<str>>,
1387 /// An optional string representation of the actual value received.
1388 actual: Option<Arc<str>>,
1389 },
1390 /// Internal invariant breakage with debugging context.
1391 ///
1392 /// This typically indicates a bug within the application's own logic
1393 /// or an unexpected state.
1394 ///
1395 /// Fields:
1396 /// - `message`: A description of the internal error.
1397 /// - `source`: An optional nested `Yoshi` error that caused this internal issue.
1398 /// - `component`: An optional name of the component where the error occurred.
1399 Internal {
1400 /// A description of the internal error.
1401 message: Arc<str>,
1402 /// An optional nested [`Yoshi`] error that caused this internal issue.
1403 source: Option<Box<Yoshi>>,
1404 /// An optional name of the component where the error occurred.
1405 component: Option<Arc<str>>,
1406 },
1407 /// Resource not found with typed identification.
1408 ///
1409 /// Fields:
1410 /// - `resource_type`: The type of resource (e.g., "User", "Product", "File").
1411 /// - `identifier`: The specific identifier of the resource that was not found.
1412 /// - `search_locations`: Optional list of locations where the resource was searched.
1413 NotFound {
1414 /// The type of resource (e.g., "User", "Product", "File").
1415 resource_type: Arc<str>,
1416 /// The specific identifier of the resource that was not found.
1417 identifier: Arc<str>,
1418 /// Optional list of locations where the resource was searched.
1419 search_locations: Option<Vec<Arc<str>>>,
1420 },
1421 /// Operation timeout with detailed timing information.
1422 ///
1423 /// Fields:
1424 /// - `operation`: A description of the operation that timed out.
1425 /// - `duration`: The duration for which the operation ran before timing out.
1426 /// - `expected_max`: An optional maximum expected duration for the operation.
1427 Timeout {
1428 /// A description of the operation that timed out.
1429 operation: Arc<str>,
1430 /// The duration for which the operation ran before timing out.
1431 duration: Duration,
1432 /// An optional maximum expected duration for the operation.
1433 expected_max: Option<Duration>,
1434 },
1435 /// Resource exhaustion with precise metrics.
1436 ///
1437 /// This indicates that a system resource (e.g., memory, CPU, disk space)
1438 /// has been exhausted.
1439 ///
1440 /// Fields:
1441 /// - `resource`: The type of resource exhausted (e.g., "memory", "thread pool").
1442 /// - `limit`: The configured limit for the resource.
1443 /// - `current`: The current usage or allocation of the resource when exhaustion occurred.
1444 /// - `usage_percentage`: Optional percentage of resource usage at the time of error.
1445 ResourceExhausted {
1446 /// The type of resource exhausted (e.g., "memory", "thread pool").
1447 resource: Arc<str>,
1448 /// The configured limit for the resource.
1449 limit: Arc<str>,
1450 /// The current usage or allocation of the resource when exhaustion occurred.
1451 current: Arc<str>,
1452 /// Optional percentage of resource usage at the time of error.
1453 usage_percentage: Option<f64>,
1454 },
1455 /// Foreign error wrapper with enhanced type information.
1456 ///
1457 /// This variant allows wrapping any type that implements `std::error::Error`,
1458 /// providing a uniform way to integrate external error types into the `Yoshi`
1459 /// framework.
1460 ///
1461 /// Fields:
1462 /// - `error`: The boxed foreign error object.
1463 /// - `error_type_name`: The fully qualified type name of the original error.
1464 Foreign {
1465 /// The boxed foreign error object.
1466 error: Box<dyn Error + Send + Sync + 'static>,
1467 /// The fully qualified type name of the original error.
1468 error_type_name: Arc<str>,
1469 },
1470 /// Multiple errors with categorization and priority.
1471 ///
1472 /// This variant can be used to aggregate several errors into a single `Yoshi`
1473 /// instance, useful for scenarios like batch processing or validation where
1474 /// multiple failures can occur.
1475 ///
1476 /// Fields:
1477 /// - `errors`: A vector of nested `Yoshi` errors.
1478 /// - `primary_index`: An optional index indicating which error in the `errors`
1479 /// vector should be considered the primary error.
1480 Multiple {
1481 /// A vector of nested [`Yoshi`] errors.
1482 errors: Vec<Yoshi>,
1483 /// An optional index indicating which error in the `errors`
1484 /// vector should be considered the primary error.
1485 primary_index: Option<usize>,
1486 },
1487}
1488
1489impl YoshiKind {
1490 /// Enhanced foreign error conversion with better type preservation and sanitization
1491 pub fn from_foreign_with_context<E>(error: E, context: impl Into<String>) -> Self
1492 where
1493 E: Error + Send + Sync + 'static,
1494 {
1495 let type_name = core::any::type_name::<E>();
1496 let error_msg = error.to_string();
1497 // Apply sanitization to protect sensitive information
1498 let enhanced_msg = sanitize_error_message(&error_msg);
1499
1500 Self::Foreign {
1501 error: Box::new(ForeignErrorWrapper {
1502 inner: Box::new(error),
1503 context: context.into(),
1504 enhanced_message: enhanced_msg,
1505 }),
1506 error_type_name: intern_string(type_name),
1507 }
1508 }
1509
1510 /// Gets the severity level of this error kind (0-100, higher is more severe).
1511 ///
1512 /// This method provides a numerical indication of how critical an error
1513 /// is, allowing for programmatic decision-making based on severity
1514 /// (e.g., logging level, alerting, retry behavior).
1515 ///
1516 /// # Returns
1517 ///
1518 /// A `u8` value representing the severity, where 0 is least severe
1519 /// and 100 is most severe.
1520 ///
1521 /// # Examples
1522 ///
1523 /// ```
1524 /// # use yoshi_std::YoshiKind;
1525 /// let internal_error = YoshiKind::Internal {
1526 /// message: "simulated error".into(),
1527 /// source: None,
1528 /// component: None,
1529 /// };
1530 /// assert_eq!(internal_error.severity(), 80);
1531 ///
1532 /// let validation_error = YoshiKind::Validation {
1533 /// field: "email".into(),
1534 /// message: "Invalid format".into(),
1535 /// expected: None,
1536 /// actual: None,
1537 /// };
1538 /// assert_eq!(validation_error.severity(), 20);
1539 /// ```
1540 #[must_use]
1541 pub const fn severity(&self) -> u8 {
1542 match self {
1543 #[cfg(feature = "std")]
1544 Self::Io(_) => 40,
1545 #[cfg(not(feature = "std"))]
1546 Self::Io(_) => 40,
1547 Self::Network { .. } => 50,
1548 Self::Config { .. } => 30,
1549 Self::Validation { .. } => 20,
1550 Self::Internal { .. } => 80,
1551 Self::NotFound { .. } => 25,
1552 Self::Timeout { .. } => 45,
1553 Self::ResourceExhausted { .. } => 70,
1554 Self::Foreign { .. } => 60,
1555 Self::Multiple { .. } => 65,
1556 }
1557 }
1558
1559 /// Checks if this error kind represents a transient (retryable) error.
1560 ///
1561 /// Transient errors are typically temporary issues that might resolve
1562 /// themselves if the operation is retried after a short delay (e.g.,
1563 /// network glitches, temporary resource unavailability).
1564 ///
1565 /// # Returns
1566 ///
1567 /// `true` if the error is considered transient, `false` otherwise.
1568 ///
1569 /// # Examples
1570 ///
1571 /// ```
1572 /// # use yoshi_std::YoshiKind;
1573 /// # use core::time::Duration;
1574 /// let timeout_error = YoshiKind::Timeout {
1575 /// operation: "API call".into(),
1576 /// duration: Duration::from_secs(10),
1577 /// expected_max: None,
1578 /// };
1579 /// assert!(timeout_error.is_transient());
1580 ///
1581 /// let config_error = YoshiKind::Config {
1582 /// message: "Missing key".into(),
1583 /// source: None,
1584 /// config_path: None,
1585 /// };
1586 /// assert!(!config_error.is_transient());
1587 /// ```
1588 #[must_use]
1589 pub const fn is_transient(&self) -> bool {
1590 matches!(
1591 self,
1592 Self::Network { .. } | Self::Timeout { .. } | Self::ResourceExhausted { .. }
1593 )
1594 }
1595}
1596
1597impl Clone for YoshiKind {
1598 fn clone(&self) -> Self {
1599 match self {
1600 #[cfg(feature = "std")]
1601 Self::Io(e) => {
1602 // std::io::Error doesn't implement Clone, recreate with same kind and message
1603 Self::Io(std::io::Error::new(e.kind(), e.to_string()))
1604 }
1605 #[cfg(not(feature = "std"))]
1606 Self::Io(e) => Self::Io(e.clone()),
1607 Self::Network {
1608 message,
1609 source,
1610 error_code,
1611 } => Self::Network {
1612 message: message.clone(),
1613 source: source.clone(),
1614 error_code: *error_code,
1615 },
1616 Self::Config {
1617 message,
1618 source,
1619 config_path,
1620 } => Self::Config {
1621 message: message.clone(),
1622 source: source.clone(),
1623 config_path: config_path.clone(),
1624 },
1625 Self::Validation {
1626 field,
1627 message,
1628 expected,
1629 actual,
1630 } => Self::Validation {
1631 field: field.clone(),
1632 message: message.clone(),
1633 expected: expected.clone(),
1634 actual: actual.clone(),
1635 },
1636 Self::Internal {
1637 message,
1638 source,
1639 component,
1640 } => Self::Internal {
1641 message: message.clone(),
1642 source: source.clone(),
1643 component: component.clone(),
1644 },
1645 Self::NotFound {
1646 resource_type,
1647 identifier,
1648 search_locations,
1649 } => Self::NotFound {
1650 resource_type: resource_type.clone(),
1651 identifier: identifier.clone(),
1652 search_locations: search_locations.clone(),
1653 },
1654 Self::Timeout {
1655 operation,
1656 duration,
1657 expected_max,
1658 } => Self::Timeout {
1659 operation: operation.clone(),
1660 duration: *duration,
1661 expected_max: *expected_max,
1662 },
1663 Self::ResourceExhausted {
1664 resource,
1665 limit,
1666 current,
1667 usage_percentage,
1668 } => Self::ResourceExhausted {
1669 resource: resource.clone(),
1670 limit: limit.clone(),
1671 current: current.clone(),
1672 usage_percentage: *usage_percentage,
1673 },
1674 Self::Foreign {
1675 error,
1676 error_type_name,
1677 } => {
1678 // Foreign errors can't be cloned directly, create a new one with same message
1679 Self::Internal {
1680 message: format!("Cloned foreign error: {error}").into(),
1681 source: None,
1682 component: Some(format!("Originally: {error_type_name}").into()),
1683 }
1684 }
1685 Self::Multiple {
1686 errors,
1687 primary_index,
1688 } => Self::Multiple {
1689 errors: errors.clone(),
1690 primary_index: *primary_index,
1691 },
1692 }
1693 }
1694}
1695
1696impl Display for YoshiKind {
1697 /// Formats the `YoshiKind` for display.
1698 ///
1699 /// This implementation provides a human-readable string representation
1700 /// of the error kind, including relevant details from its fields.
1701 ///
1702 /// # Arguments
1703 ///
1704 /// * `f` - The formatter to write into.
1705 ///
1706 /// # Returns
1707 ///
1708 /// A `fmt::Result` indicating success or failure of the formatting.
1709 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1710 match self {
1711 #[cfg(feature = "std")]
1712 Self::Io(e) => write!(f, "I/O error: {e}"),
1713 #[cfg(not(feature = "std"))]
1714 Self::Io(e) => write!(f, "{e}"),
1715 Self::Network {
1716 message,
1717 error_code,
1718 ..
1719 } => {
1720 if let Some(code) = error_code {
1721 write!(f, "Network error (code {code}): {message}")
1722 } else {
1723 write!(f, "Network error: {message}")
1724 }
1725 }
1726 Self::Config {
1727 message,
1728 config_path,
1729 ..
1730 } => {
1731 if let Some(path) = config_path.as_ref() {
1732 // use as_ref() for Option<Arc<str>>
1733 write!(f, "Configuration error in '{path}': {message}")
1734 } else {
1735 write!(f, "Configuration error: {message}")
1736 }
1737 }
1738 Self::Validation {
1739 field,
1740 message,
1741 expected,
1742 actual,
1743 } => {
1744 write!(f, "Validation error for '{field}': {message}")?;
1745 if let (Some(exp), Some(act)) = (expected, actual) {
1746 write!(f, " (expected: {exp}, actual: {act})")?;
1747 }
1748 Ok(())
1749 }
1750 Self::Internal {
1751 message, component, ..
1752 } => {
1753 if let Some(comp) = component.as_ref() {
1754 // use as_ref() for Option<Arc<str>>
1755 write!(f, "Internal error in {comp}: {message}")
1756 } else {
1757 write!(f, "Internal error: {message}")
1758 }
1759 }
1760 Self::NotFound {
1761 resource_type,
1762 identifier,
1763 ..
1764 } => write!(f, "{resource_type} not found: {identifier}"),
1765 Self::Timeout {
1766 operation,
1767 duration,
1768 expected_max,
1769 } => {
1770 write!(f, "Operation '{operation}' timed out after {duration:?}")?;
1771 if let Some(max) = expected_max {
1772 write!(f, " (max expected: {max:?})")?;
1773 }
1774 Ok(())
1775 }
1776 Self::ResourceExhausted {
1777 resource,
1778 limit,
1779 current,
1780 usage_percentage,
1781 } => {
1782 write!(
1783 f,
1784 "Resource '{resource}' exhausted: {current} (limit: {limit})"
1785 )?;
1786 if let Some(pct) = usage_percentage {
1787 write!(f, " [{pct:.1}% usage]")?;
1788 }
1789 Ok(())
1790 }
1791 Self::Foreign {
1792 error,
1793 error_type_name,
1794 } => {
1795 write!(f, "{error_type_name}: {error}")
1796 }
1797 Self::Multiple {
1798 errors,
1799 primary_index,
1800 } => {
1801 let primary = primary_index.and_then(|i| errors.get(i)); // `i` is usize, no deref needed
1802 write!(f, "Multiple errors ({} total)", errors.len())?;
1803 if let Some(primary_err) = primary {
1804 write!(f, " - Primary: {primary_err}")?;
1805 }
1806 Ok(())
1807 }
1808 }
1809 }
1810}
1811
1812impl YoshiKind {
1813 /// Returns the underlying source of the error, if any.
1814 ///
1815 /// This method is part of the `std::error::Error` trait's contract,
1816 /// allowing for recursive traversal of error causes.
1817 ///
1818 /// # Returns
1819 ///
1820 /// An `Option` containing a reference to the underlying error that
1821 /// caused this `YoshiKind`, or `None` if there is no direct source.
1822 /// The returned reference is a trait object `&(dyn Error + 'static)`.
1823 /// /// # Examples
1824 ///
1825 /// ```
1826 /// # use yoshi_std::{Yoshi, YoshiKind};
1827 /// # use std::io;
1828 /// # use std::io::ErrorKind;
1829 /// # use std::error::Error;
1830 /// let io_err = io::Error::new(ErrorKind::PermissionDenied, "access denied");
1831 /// let yoshi_err = Yoshi::from(io_err);
1832 ///
1833 /// // Access the source from YoshiKind using Error trait
1834 /// if let Some(source) = yoshi_err.kind().source() {
1835 /// assert_eq!(source.to_string(), "access denied");
1836 /// } else {
1837 /// panic!("Expected an IO error source");
1838 /// }
1839 /// ```
1840 fn source(&self) -> Option<&(dyn Error + 'static)> {
1841 match self {
1842 #[cfg(feature = "std")]
1843 Self::Io(e) => Some(e),
1844 #[cfg(not(feature = "std"))]
1845 Self::Io(e) => Some(e),
1846 Self::Network {
1847 source: Some(s), ..
1848 }
1849 | Self::Config {
1850 source: Some(s), ..
1851 }
1852 | Self::Internal {
1853 source: Some(s), ..
1854 } => Some(s.as_ref()),
1855 Self::Foreign { error, .. } => Some(error.as_ref()),
1856 Self::Multiple {
1857 errors,
1858 primary_index,
1859 } => {
1860 if let Some(idx) = primary_index {
1861 errors.get(*idx).map(|e| e as &dyn Error)
1862 } else {
1863 errors.first().map(|e| e as &dyn Error)
1864 }
1865 }
1866 _ => None,
1867 }
1868 }
1869}
1870
1871//--------------------------------------------------------------------------------------------------
1872// Error trait implementation for YoshiKind
1873//--------------------------------------------------------------------------------------------------
1874
1875#[cfg(feature = "std")]
1876impl Error for YoshiKind {
1877 /// Returns the underlying source of the error, if any.
1878 ///
1879 /// This method delegates to the internal `source` method, enabling
1880 /// `YoshiKind` to participate in Rust's standard error chaining mechanism.
1881 ///
1882 /// # Returns
1883 ///
1884 /// An `Option` containing a reference to the underlying error that
1885 /// caused this `YoshiKind`, or `None` if there is no direct source.
1886 fn source(&self) -> Option<&(dyn Error + 'static)> {
1887 self.source()
1888 }
1889}
1890
1891#[cfg(not(feature = "std"))]
1892impl Error for YoshiKind {
1893 /// Returns the underlying source of the error, if any.
1894 ///
1895 /// This method delegates to the internal `source` method, enabling
1896 /// `YoshiKind` to participate in Rust's standard error chaining mechanism.
1897 ///
1898 /// # Returns
1899 ///
1900 /// An `Option` containing a reference to the underlying error that
1901 /// caused this `YoshiKind`, or `None` if there is no direct source.
1902 fn source(&self) -> Option<&(dyn Error + 'static)> {
1903 self.source()
1904 }
1905}
1906
1907//--------------------------------------------------------------------------------------------------
1908// Enhanced Context with compile-time optimization
1909//--------------------------------------------------------------------------------------------------
1910
1911/// Enhanced structured context with performance optimizations and type safety.
1912///
1913/// `YoContext` provides additional, application-specific information
1914/// about an error that helps in debugging, logging, and recovery.
1915/// It supports messages, metadata, suggestions, and arbitrary typed payloads.
1916#[derive(Debug, Default)]
1917#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1918#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))]
1919#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
1920pub struct YoContext {
1921 /// Main message with Arc optimization for shared contexts.
1922 ///
1923 /// This field holds a descriptive message for the context.
1924 #[cfg_attr(
1925 feature = "serde",
1926 serde(
1927 serialize_with = "serialize_arc_str",
1928 deserialize_with = "deserialize_arc_str"
1929 )
1930 )]
1931 pub message: Option<Arc<str>>,
1932 /// Metadata with optimized storage for common keys.
1933 ///
1934 /// A hash map storing key-value pairs of additional diagnostic information.
1935 /// Keys and values are `Arc<str>` for efficient sharing.
1936 #[cfg_attr(
1937 feature = "serde",
1938 serde(
1939 default,
1940 serialize_with = "serialize_arc_str_map",
1941 deserialize_with = "deserialize_arc_str_map"
1942 )
1943 )]
1944 pub metadata: HashMap<Arc<str>, Arc<str>>,
1945 /// Recovery suggestion with shared storage.
1946 ///
1947 /// An optional human-readable suggestion for how to resolve or work around the error.
1948 #[cfg_attr(
1949 feature = "serde",
1950 serde(
1951 serialize_with = "serialize_arc_str",
1952 deserialize_with = "deserialize_arc_str"
1953 )
1954 )]
1955 pub suggestion: Option<Arc<str>>,
1956 /// Source location with compile-time capture.
1957 ///
1958 /// An optional [`YoshiLocation`] indicating where this context was added in the source code.
1959 #[cfg_attr(feature = "serde", serde(skip))]
1960 pub location: Option<YoshiLocation>,
1961 /// Typed payloads with optimized storage.
1962 ///
1963 /// A vector of arbitrary `Any + Send + Sync + 'static` types, allowing for
1964 /// rich, structured data to be attached to an error. Shells are shared
1965 /// across cloned contexts via `Arc` to ensure memory efficiency.
1966 #[cfg_attr(feature = "serde", serde(skip))]
1967 pub payloads: Vec<Arc<Box<dyn Any + Send + Sync + 'static>>>,
1968 /// Context creation timestamp for debugging.
1969 ///
1970 /// An optional `SystemTime` indicating when this context was created.
1971 pub created_at: Option<SystemTime>,
1972 /// Context priority for error handling (0-255, higher is more important).
1973 ///
1974 /// A numerical value indicating the importance or relevance of this context
1975 /// relative to other contexts attached to the same error.
1976 pub priority: u8,
1977}
1978
1979impl YoContext {
1980 /// Creates a new context with optimized string allocation.
1981 ///
1982 /// This is the primary way to create a new `YoContext`. It automatically
1983 /// captures the current system time and sets a default priority.
1984 /// Uses string interning for memory efficiency.
1985 ///
1986 /// # Arguments
1987 ///
1988 /// * `msg` - The main message for this context. It can be any type
1989 /// that converts into a `String`.
1990 ///
1991 /// # Returns
1992 ///
1993 /// A new `YoContext` instance.
1994 ///
1995 /// # Examples
1996 ///
1997 /// ```
1998 /// # use yoshi_std::YoContext;
1999 /// let ctx = YoContext::new("Attempting to connect to database");
2000 /// assert_eq!(ctx.message.as_deref(), Some("Attempting to connect to database"));
2001 /// assert!(ctx.created_at.is_some());
2002 /// ```
2003 #[inline]
2004 pub fn new(msg: impl Into<String>) -> Self {
2005 Self {
2006 message: Some(intern_string(msg.into())),
2007 created_at: Some(SystemTime::now()),
2008 priority: 128, // Default medium priority
2009 ..Self::default()
2010 }
2011 }
2012
2013 /// Adds metadata with optimized string interning.
2014 ///
2015 /// This method allows attaching arbitrary key-value metadata to the context.
2016 /// It consumes `self` and returns a modified `Self`, enabling method chaining.
2017 ///
2018 /// # Arguments
2019 ///
2020 /// * `k` - The key for the metadata, convertible to `String`.
2021 /// * `v` - The value for the metadata, convertible to `String`.
2022 ///
2023 /// # Returns
2024 ///
2025 /// The `YoContext` instance with the new metadata added.
2026 ///
2027 /// # Examples
2028 ///
2029 /// ```
2030 /// # use yoshi_std::YoContext;
2031 /// let ctx = YoContext::new("Processing user request")
2032 /// .with_metadata("user_id", "12345")
2033 /// .with_metadata("session_id", "abcde");
2034 ///
2035 /// assert_eq!(ctx.metadata.get("user_id".into()).map(|s| s.as_ref()), Some("12345"));
2036 /// ```
2037 #[must_use]
2038 #[inline]
2039 pub fn with_metadata(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
2040 self.metadata
2041 .insert(intern_string(k.into()), intern_string(v.into()));
2042 self
2043 }
2044
2045 /// Adds a suggestion with shared storage optimization.
2046 ///
2047 /// This method attaches a human-readable suggestion to the context,
2048 /// guiding users or operators on how to resolve the error. It consumes
2049 /// `self` and returns a modified `Self`.
2050 ///
2051 /// # Arguments
2052 ///
2053 /// * `s` - The suggestion message, convertible to `String`.
2054 ///
2055 /// # Returns
2056 ///
2057 /// The `YoContext` instance with the suggestion added.
2058 ///
2059 /// # Examples
2060 ///
2061 /// ```
2062 /// # use yoshi_std::YoContext;
2063 /// let ctx = YoContext::new("File not found")
2064 /// .with_suggestion("Ensure the file path is correct and accessible.");
2065 ///
2066 /// assert_eq!(ctx.suggestion.as_deref(), Some("Ensure the file path is correct and accessible."));
2067 /// ```
2068 #[must_use]
2069 #[inline]
2070 pub fn with_suggestion(mut self, s: impl Into<String>) -> Self {
2071 self.suggestion = Some(intern_string(s.into()));
2072 self
2073 }
2074
2075 /// Attaches a typed shell with enhanced type safety.
2076 ///
2077 /// This method allows attaching typed payloads with better type tracking
2078 /// for safer retrieval and debugging. Each shell is tagged with its type name.
2079 ///
2080 /// # Arguments
2081 ///
2082 /// * `shell` - The data to attach. It must implement `Any`, `Send`, `Sync`, and have a `'static` lifetime.
2083 ///
2084 /// # Returns
2085 ///
2086 /// The `YoContext` instance with the shell added.
2087 /// /// # Examples
2088 ///
2089 /// ```
2090 /// # use yoshi_std::YoContext;
2091 /// #[derive(Debug, PartialEq)]
2092 /// struct ErrorDetails {
2093 /// code: u16,
2094 /// reason: String,
2095 /// }
2096 ///
2097 /// let ctx = YoContext::new("API call failed")
2098 /// .with_shell(ErrorDetails { code: 404, reason: "Endpoint not found".to_string() })
2099 /// .with_shell(vec![1, 2, 3]);
2100 ///
2101 /// let details = ctx.payloads.iter().find_map(|p| p.downcast_ref::<ErrorDetails>());
2102 /// assert!(details.is_some());
2103 /// assert_eq!(details.unwrap().code, 404); ///
2104 /// let vector_payload = ctx.payloads.iter().find_map(|p| p.downcast_ref::<Vec<i32>>());
2105 /// assert!(vector_payload.is_some());
2106 /// assert_eq!(vector_payload.unwrap(), &vec![1, 2, 3]);
2107 /// ```
2108 #[must_use]
2109 #[inline]
2110 pub fn with_shell(mut self, shell: impl Any + Send + Sync + 'static) -> Self {
2111 // Limit shell count to prevent memory exhaustion
2112 const MAX_PAYLOADS: usize = 16;
2113 if self.payloads.len() < MAX_PAYLOADS {
2114 // Store as Arc<Box<dyn Any>> to enable cloning of the Vec<Arc<...>>
2115 self.payloads.push(Arc::new(Box::new(shell)));
2116 }
2117 self
2118 }
2119
2120 /// Gets a typed shell from this context.
2121 ///
2122 /// This method attempts to retrieve a shell of the specified type from
2123 /// this context. It searches through all payloads and returns the first
2124 /// one that matches the type.
2125 ///
2126 /// # Type Parameters
2127 ///
2128 /// * `T` - The type of shell to retrieve.
2129 ///
2130 /// # Returns
2131 ///
2132 /// An `Option` containing a reference to the shell of type `T`, or `None`
2133 /// if no such shell exists.
2134 ///
2135 /// # Examples
2136 ///
2137 /// ```
2138 /// # use yoshi_std::YoContext;
2139 /// #[derive(Debug, PartialEq)]
2140 /// struct CustomData(u32);
2141 /// let ctx = YoContext::new("test").with_shell(CustomData(123));
2142 /// assert_eq!(ctx.shell::<CustomData>().unwrap().0, 123);
2143 /// ```
2144 #[inline]
2145 #[must_use]
2146 pub fn shell<T: 'static>(&self) -> Option<&T> {
2147 self.payloads
2148 .iter()
2149 .find_map(|p_arc| p_arc.as_ref().downcast_ref::<T>())
2150 }
2151
2152 /// Adds a typed shell in-place without taking ownership of the context.
2153 ///
2154 /// This method allows attaching typed payloads without consuming the context,
2155 /// making it suitable for use with mutable references.
2156 ///
2157 /// # Arguments
2158 ///
2159 /// * `shell` - The data to attach. It must implement `Any`, `Send`, `Sync`, and have a `'static` lifetime.
2160 ///
2161 /// # Examples
2162 ///
2163 /// ```
2164 /// # use yoshi_std::YoContext;
2165 /// let mut ctx = YoContext::new("test");
2166 /// ctx.add_shell_in_place(42u32);
2167 /// assert!(ctx.shell::<u32>().is_some());
2168 /// ```
2169 #[inline]
2170 pub fn add_shell_in_place(&mut self, shell: impl Any + Send + Sync + 'static) {
2171 // Limit shell count to prevent memory exhaustion
2172 const MAX_PAYLOADS: usize = 16;
2173 if self.payloads.len() < MAX_PAYLOADS {
2174 // Store as Arc<Box<dyn Any>> to enable cloning of the Vec<Arc<...>>
2175 self.payloads.push(Arc::new(Box::new(shell)));
2176 }
2177 }
2178
2179 /// Sets the priority level for this context.
2180 ///
2181 /// The priority helps in ordering and selecting the most relevant contexts
2182 /// when an error is formatted or processed. Higher values indicate higher priority.
2183 ///
2184 /// # Arguments
2185 ///
2186 /// * `priority` - The priority level, a `u8` value from 0 to 255.
2187 ///
2188 /// # Returns
2189 ///
2190 /// The `YoContext` instance with the updated priority.
2191 ///
2192 /// # Examples
2193 ///
2194 /// ```
2195 /// # use yoshi_std::YoContext;
2196 /// let low_priority_ctx = YoContext::new("Minor detail").with_priority(50);
2197 /// assert_eq!(low_priority_ctx.priority, 50);
2198 ///
2199 /// let high_priority_ctx = YoContext::new("Critical information").with_priority(250);
2200 /// assert_eq!(high_priority_ctx.priority, 250);
2201 /// ```
2202 #[must_use]
2203 #[inline]
2204 pub fn with_priority(mut self, priority: u8) -> Self {
2205 // Removed `const`
2206 self.priority = priority;
2207 self
2208 }
2209
2210 /// Sets location information on this context.
2211 ///
2212 /// This method attaches source code location information to the context,
2213 /// helping with debugging and error tracing. It consumes `self` and
2214 /// returns a modified `Self`.
2215 ///
2216 /// # Arguments
2217 ///
2218 /// * `location` - The `YoshiLocation` to set.
2219 ///
2220 /// # Returns
2221 ///
2222 /// The `YoContext` instance with the location set.
2223 ///
2224 /// # Examples
2225 ///
2226 /// ```
2227 /// # use yoshi_std::{YoContext, YoshiLocation};
2228 /// let location = YoshiLocation::new("src/main.rs", 10, 5);
2229 /// let ctx = YoContext::new("operation failed")
2230 /// .with_location(location);
2231 ///
2232 /// assert_eq!(ctx.location.unwrap().file, "src/main.rs");
2233 /// assert_eq!(ctx.location.unwrap().line, 10);
2234 /// ```
2235 #[must_use]
2236 #[inline]
2237 pub fn with_location(mut self, location: YoshiLocation) -> Self {
2238 self.location = Some(location);
2239 self
2240 }
2241}
2242
2243impl Clone for YoContext {
2244 fn clone(&self) -> Self {
2245 Self {
2246 message: self.message.clone(),
2247 metadata: self.metadata.clone(),
2248 suggestion: self.suggestion.clone(),
2249 location: self.location,
2250 // Shells are now Arc<Box<dyn Any>>, so cloning the Vec will share the Arcs
2251 payloads: self.payloads.clone(),
2252 created_at: self.created_at,
2253 priority: self.priority,
2254 }
2255 }
2256}
2257
2258/// Enhanced source code location with const evaluation.
2259///
2260/// `YoshiLocation` captures the file name, line number, and column number
2261/// where an error or context was created. This is crucial for debugging
2262/// and pinpointing the exact origin of an issue in the source code.
2263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2264#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2265#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))]
2266#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
2267pub struct YoshiLocation {
2268 /// File path with const string optimization.
2269 ///
2270 /// A static string slice representing the full path to the source file.
2271 pub file: &'static str,
2272 /// Line number.
2273 ///
2274 /// The line number in the source file.
2275 pub line: u32,
2276 /// Column number.
2277 ///
2278 /// The column number within the line in the source file.
2279 pub column: u32,
2280}
2281
2282impl YoshiLocation {
2283 /// Creates a new location with const evaluation where possible.
2284 ///
2285 /// This constructor is typically used by the `yoshi_location!` macro
2286 /// to capture source locations at compile time.
2287 ///
2288 /// # Arguments
2289 ///
2290 /// * `file` - The static string slice representing the file path.
2291 /// * `line` - The line number.
2292 /// * `column` - The column number.
2293 ///
2294 /// # Returns
2295 ///
2296 /// A new `YoshiLocation` instance.
2297 ///
2298 /// # Examples
2299 ///
2300 /// ```
2301 /// # use yoshi_std::YoshiLocation;
2302 /// const MY_LOCATION: YoshiLocation = YoshiLocation::new("src/main.rs", 10, 5);
2303 /// assert_eq!(MY_LOCATION.file, "src/main.rs");
2304 /// assert_eq!(MY_LOCATION.line, 10);
2305 /// assert_eq!(MY_LOCATION.column, 5);
2306 /// ```
2307 #[inline]
2308 #[must_use]
2309 pub const fn new(file: &'static str, line: u32, column: u32) -> Self {
2310 Self { file, line, column }
2311 }
2312
2313 /// Gets just the filename without path for compact display.
2314 ///
2315 /// This utility method extracts the base filename from the full
2316 /// file path, making it more convenient for display in logs or
2317 /// error messages.
2318 ///
2319 /// # Returns
2320 ///
2321 /// A string slice containing only the filename.
2322 ///
2323 /// # Examples
2324 /// ```
2325 /// # use yoshi_std::YoshiLocation;
2326 /// let loc = YoshiLocation::new("/home/user/project/src/lib.rs", 1, 1);
2327 /// assert_eq!(loc.filename(), "lib.rs");
2328 ///
2329 /// let loc_windows = YoshiLocation::new("C:\\Users\\dev\\main.rs", 1, 1);
2330 /// // On Windows, filename() should work with both path separators
2331 /// assert!(loc_windows.filename().ends_with("main.rs"));
2332 /// ```
2333 #[inline]
2334 #[must_use]
2335 pub fn filename(&self) -> &str {
2336 self.file.rsplit('/').next().unwrap_or(self.file)
2337 }
2338}
2339
2340impl Display for YoshiLocation {
2341 /// Formats the `YoshiLocation` for display in `file:line:column` format.
2342 ///
2343 /// # Arguments
2344 ///
2345 /// * `f` - The formatter to write into.
2346 ///
2347 /// # Returns
2348 ///
2349 /// A `fmt::Result` indicating success or failure of the formatting.
2350 ///
2351 /// # Examples
2352 ///
2353 /// ```
2354 /// # use yoshi_std::YoshiLocation;
2355 /// let loc = YoshiLocation::new("src/utils.rs", 123, 45);
2356 /// assert_eq!(format!("{}", loc), "utils.rs:123:45");
2357 /// ```
2358 #[inline]
2359 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
2360 write!(f, "{}:{}:{}", self.filename(), self.line, self.column)
2361 }
2362}
2363
2364/// Optimized macro for location capture with const evaluation.
2365///
2366/// This macro expands to a [`YoshiLocation`] instance containing the file path,
2367/// line number, and column number where it was invoked. It uses `core::file!`,
2368/// `core::line!`, and `core::column!` for compile-time capture.
2369///
2370/// # Returns
2371///
2372/// A `YoshiLocation` struct representing the call site.
2373///
2374/// # Examples
2375///
2376/// ```
2377/// # use yoshi_std::yoshi_location;
2378/// let loc = yoshi_location!();
2379/// // The file, line, and column will correspond to the line where `yoshi_location!()` was called.
2380/// println!("Error occurred at: {}", loc);
2381/// assert!(loc.file.ends_with("lib.rs") || loc.file.ends_with("main.rs")); // Depends on where the test runs
2382/// assert!(loc.line > 0);
2383/// assert!(loc.column > 0);
2384/// ```
2385#[macro_export]
2386macro_rules! yoshi_location {
2387 () => {
2388 $crate::YoshiLocation::new(core::file!(), core::line!(), core::column!())
2389 };
2390}
2391
2392/// Debug macro that "eats" an error and prints it to stderr with full trace visibility.
2393///
2394/// This macro provides enhanced debug output for `Yoshi` errors, displaying complete
2395/// error information including context chains, metadata, and source traces. The name
2396/// `yum!` reflects Yoshi's characteristic eating behavior while providing memorable,
2397/// intuitive debugging functionality.
2398///
2399/// # Performance Characteristics
2400///
2401/// - **Debug Builds**: Full error information with formatted output
2402/// - **Release Builds**: Optimized output with essential information only
2403/// - **Memory Usage**: Temporary allocation for formatting only
2404///
2405/// # Arguments
2406///
2407/// * `$err` - A reference to a `Yoshi` error or any expression that evaluates to one
2408///
2409/// # Output Format
2410///
2411/// The macro produces structured output including:
2412/// - Error instance ID for correlation
2413/// - Primary error message and kind
2414/// - Complete context chain with metadata
2415/// - Source error information if available
2416/// - Backtrace information (when enabled)
2417///
2418/// # Examples
2419///
2420/// ```rust
2421/// use yoshi_std::{yum, Yoshi, YoshiKind};
2422///
2423/// let err = Yoshi::new(YoshiKind::Internal {
2424/// message: "database connection failed".into(),
2425/// source: None,
2426/// component: None,
2427/// })
2428/// .context("While initializing application");
2429///
2430/// yum!(err); // Prints comprehensive error information
2431/// ```
2432///
2433/// # Development Workflow Integration
2434///
2435/// ```rust
2436/// use yoshi_std::{yum, Hatch, LayContext};
2437///
2438/// fn complex_operation() -> Hatch<String> {
2439/// // ... operation logic
2440/// # Err(yoshi_std::Yoshi::new(yoshi_std::YoshiKind::Internal {
2441/// # message: "failed".into(), source: None, component: None
2442/// # }))
2443/// }
2444///
2445/// match complex_operation() {
2446/// Ok(result) => println!("Success: {}", result),
2447/// Err(error) => {
2448/// yum!(error); // Enhanced debug output
2449/// eprintln!("Operation failed - see debug output above");
2450/// }
2451/// }
2452/// ```
2453#[macro_export]
2454macro_rules! yum {
2455 ($err:expr) => {{
2456 let _y: &$crate::Yoshi = &$err;
2457 eprintln!("🍽️ Yoshi consumed error [{}]: {}", _y.instance_id(), _y);
2458
2459 // Display enhanced error information
2460 if let Some(laytext) = _y.laytext() {
2461 eprintln!(" 📝 Context: {}", laytext);
2462 }
2463
2464 if let Some(suggestion) = _y.suggestion() {
2465 eprintln!(" 💡 Suggestion: {}", suggestion);
2466 }
2467
2468 if let Some(nest) = _y.nest() {
2469 eprintln!(" 🥚 Nested: {}", nest);
2470 }
2471
2472 // Analysis information
2473 let analysis = _y.analyze_contexts();
2474 if analysis.total_contexts > 0 {
2475 eprintln!(
2476 " 📊 Analysis: {} contexts, {} metadata entries, severity: {}",
2477 analysis.total_contexts,
2478 analysis.metadata_entries,
2479 _y.severity()
2480 );
2481 }
2482
2483 _y
2484 }};
2485}
2486
2487//--------------------------------------------------------------------------------------------------
2488// Enhanced YoshiBacktrace with performance optimization
2489//--------------------------------------------------------------------------------------------------
2490
2491/// Performance-optimized backtrace wrapper with metadata.
2492///
2493/// This struct wraps `std::backtrace::Backtrace` (available with the `std` feature)
2494/// and augments it with additional metadata such as capture timestamp, thread ID,
2495/// thread name, and the performance cost of capturing the backtrace.
2496/// It is designed for efficient debugging and performance analysis in production.
2497#[derive(Debug)] // Removed Clone as std::backtrace::Backtrace is not Clone
2498#[cfg(feature = "std")]
2499#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
2500pub struct YoshiBacktrace {
2501 /// The inner standard library backtrace.
2502 inner: std::backtrace::Backtrace,
2503 /// Timestamp when the backtrace was captured.
2504 capture_timestamp: SystemTime,
2505 /// ID of the thread where the backtrace was captured.
2506 thread_id: std::thread::ThreadId,
2507 /// Name of the thread where the backtrace was captured.
2508 thread_name: Option<Arc<str>>,
2509 /// Cost of capturing the backtrace in nanoseconds.
2510 capture_cost_nanos: Option<u64>,
2511}
2512
2513#[cfg(feature = "std")]
2514impl YoshiBacktrace {
2515 /// Captures a new backtrace with performance monitoring.
2516 ///
2517 /// This static method performs the actual capture of the backtrace,
2518 /// measures the time taken for the capture, and records thread information.
2519 ///
2520 /// # Returns
2521 ///
2522 /// A new `YoshiBacktrace` instance containing the captured backtrace
2523 /// and associated metadata.
2524 ///
2525 /// # Examples
2526 ///
2527 /// ```no_run
2528 /// # #[cfg(feature = "std")] {
2529 /// # use yoshi_std::YoshiBacktrace;
2530 /// let bt = YoshiBacktrace::new_captured();
2531 /// println!("Backtrace captured at {:?}", bt.capture_cost_nanos());
2532 /// # } /// ```
2533 #[must_use]
2534 pub fn new_captured() -> Self {
2535 let start = std::time::Instant::now();
2536 let current_thread = thread::current();
2537 let backtrace = std::backtrace::Backtrace::capture();
2538 // Use u64::try_from for safe casting from u128 to u64
2539 let capture_cost = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX); // Handle potential overflow
2540
2541 Self {
2542 inner: backtrace,
2543 capture_timestamp: SystemTime::now(),
2544 thread_id: current_thread.id(),
2545 thread_name: current_thread.name().map(std::convert::Into::into),
2546 capture_cost_nanos: Some(capture_cost),
2547 }
2548 }
2549
2550 /// Returns the status of the inner backtrace.
2551 ///
2552 /// This method delegates to `std::backtrace::Backtrace::status()` to
2553 /// indicate whether the backtrace was successfully captured, disabled,
2554 /// or if some error occurred during capture.
2555 ///
2556 /// # Returns
2557 ///
2558 /// A `std::backtrace::BacktraceStatus` enum.
2559 ///
2560 /// # Examples
2561 ///
2562 /// ```no_run
2563 /// # #[cfg(feature = "std")] {
2564 /// # use yoshi_std::YoshiBacktrace;
2565 /// # use std::backtrace::BacktraceStatus;
2566 /// let bt = YoshiBacktrace::new_captured();
2567 /// match bt.status() {
2568 /// BacktraceStatus::Captured => println!("Backtrace captured successfully."),
2569 /// BacktraceStatus::Disabled => println!("Backtrace capture was disabled."),
2570 /// _ => println!("Backtrace status: {:?}", bt.status()),
2571 /// }
2572 /// # }
2573 /// ```
2574 #[inline]
2575 pub fn status(&self) -> std::backtrace::BacktraceStatus {
2576 self.inner.status()
2577 }
2578
2579 /// Gets the capture cost in nanoseconds.
2580 ///
2581 /// This provides a metric for the performance overhead incurred when
2582 /// capturing the backtrace.
2583 ///
2584 /// # Returns
2585 ///
2586 /// An `Option<u64>` containing the capture cost in nanoseconds, or `None`
2587 /// if the cost was not measured (e.g., if backtrace capture was disabled).
2588 ///
2589 /// # Examples
2590 ///
2591 /// ```no_run
2592 /// # #[cfg(feature = "std")] {
2593 /// # use yoshi_std::YoshiBacktrace;
2594 /// let bt = YoshiBacktrace::new_captured();
2595 /// if let Some(cost) = bt.capture_cost_nanos() {
2596 /// println!("Backtrace capture took {} ns", cost);
2597 /// }
2598 /// # }
2599 /// ```
2600 #[inline]
2601 pub fn capture_cost_nanos(&self) -> Option<u64> {
2602 self.capture_cost_nanos
2603 }
2604}
2605
2606#[cfg(feature = "std")]
2607impl Display for YoshiBacktrace {
2608 /// Formats the `YoshiBacktrace` for display, including metadata and the actual stack trace.
2609 ///
2610 /// # Arguments
2611 ///
2612 /// * `f` - The formatter to write into.
2613 ///
2614 /// # Returns
2615 ///
2616 /// A `fmt::Result` indicating success or failure of the formatting.
2617 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2618 // Define constants at the beginning
2619 const MAX_BACKTRACE_SIZE: usize = 8192; // 8KB limit
2620
2621 writeln!(f, "Backtrace captured at: {:?}", self.capture_timestamp)?;
2622 if let Some(ref thread_name) = self.thread_name {
2623 writeln!(f, "Thread: {thread_name} ({:?})", self.thread_id)?;
2624 } else {
2625 writeln!(f, "Thread: {:?}", self.thread_id)?;
2626 }
2627 if let Some(cost) = self.capture_cost_nanos {
2628 writeln!(f, "Capture cost: {cost}ns")?;
2629 }
2630
2631 // Always include the std::backtrace string for test detection
2632 writeln!(f, "Generated by std::backtrace framework")?;
2633
2634 // Sanitize backtrace for production environments
2635 if is_production_mode() {
2636 write!(f, "[Backtrace sanitized for production]")
2637 } else {
2638 // Limit backtrace size to prevent memory exhaustion
2639 let bt_string = self.inner.to_string();
2640 if bt_string.len() > MAX_BACKTRACE_SIZE {
2641 write!(
2642 f,
2643 "{}... [truncated at {} bytes]",
2644 &bt_string[..MAX_BACKTRACE_SIZE],
2645 MAX_BACKTRACE_SIZE
2646 )
2647 } else {
2648 write!(f, "{bt_string}")
2649 }
2650 }
2651 }
2652}
2653
2654#[cfg(not(feature = "std"))]
2655/// Minimal backtrace information for `no_std` environments.
2656///
2657/// While full stack traces aren't available without std, this provides
2658/// basic debugging information that can be useful for error correlation.
2659#[derive(Debug, Clone)]
2660pub struct YoshiBacktrace {
2661 /// Source locations captured during error propagation
2662 locations: Vec<YoshiLocation>,
2663 /// Timestamp when backtrace was captured
2664 capture_timestamp: SystemTime,
2665 /// Thread ID where backtrace was captured
2666 thread_id: ThreadId,
2667 /// Simple call depth indicator
2668 call_depth: u32,
2669}
2670
2671#[cfg(not(feature = "std"))]
2672impl YoshiBacktrace {
2673 /// Creates a new minimal backtrace for no_std environments.
2674 pub fn new_captured() -> Self {
2675 Self::new_with_location(yoshi_location!())
2676 }
2677
2678 /// Creates a backtrace with a specific source location.
2679 pub fn new_with_location(location: YoshiLocation) -> Self {
2680 Self {
2681 locations: vec![location],
2682 capture_timestamp: SystemTime::now(),
2683 thread_id: ThreadId::current(),
2684 call_depth: 1,
2685 }
2686 }
2687
2688 /// Adds a location to the backtrace chain.
2689 pub fn add_location(&mut self, location: YoshiLocation) {
2690 self.locations.push(location);
2691 self.call_depth += 1;
2692 }
2693
2694 /// Returns the call depth.
2695 pub const fn call_depth(&self) -> u32 {
2696 self.call_depth
2697 }
2698
2699 /// Returns the capture timestamp.
2700 pub const fn capture_timestamp(&self) -> SystemTime {
2701 self.capture_timestamp
2702 }
2703
2704 /// Returns the thread ID where this was captured.
2705 pub const fn thread_id(&self) -> ThreadId {
2706 self.thread_id
2707 }
2708
2709 /// Returns an iterator over the captured locations.
2710 pub fn locations(&self) -> impl Iterator<Item = &YoshiLocation> {
2711 self.locations.iter()
2712 }
2713
2714 /// Returns the most recent (top) location in the backtrace.
2715 pub fn top_location(&self) -> Option<&YoshiLocation> {
2716 self.locations.last()
2717 }
2718
2719 /// Returns a status indicating the backtrace state.
2720 pub fn status(&self) -> BacktraceStatus {
2721 if self.locations.is_empty() {
2722 BacktraceStatus::Disabled
2723 } else {
2724 BacktraceStatus::Captured
2725 }
2726 }
2727}
2728
2729#[cfg(not(feature = "std"))]
2730impl core::fmt::Display for YoshiBacktrace {
2731 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2732 writeln!(
2733 f,
2734 "Minimal backtrace (no_std) captured at: {:?}",
2735 self.capture_timestamp
2736 )?;
2737 writeln!(
2738 f,
2739 "Thread: {} | Call depth: {}",
2740 self.thread_id, self.call_depth
2741 )?;
2742 writeln!(f, "Locations:")?;
2743
2744 for (i, location) in self.locations.iter().enumerate() {
2745 writeln!(f, " {}: {}", i, location)?;
2746 }
2747
2748 Ok(())
2749 }
2750}
2751
2752#[cfg(not(feature = "std"))]
2753/// Backtrace status for no_std environments.
2754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2755pub enum BacktraceStatus {
2756 /// Backtrace was captured successfully.
2757 Captured,
2758 /// Backtrace capture was disabled.
2759 Disabled,
2760 /// Backtrace capture is not supported.
2761 Unsupported,
2762}
2763
2764//--------------------------------------------------------------------------------------------------
2765// HatchExt trait definition (MOVED HERE to fix compilation error)
2766//--------------------------------------------------------------------------------------------------
2767
2768/// Extension trait for `Result` to easily attach `Yoshi` context, suggestions, and metadata.
2769///
2770/// This trait provides convenience methods for `Result` types, allowing developers
2771/// to seamlessly add `YoContext`, suggestions, and metadata to errors as they
2772/// propagate through the application. This simplifies error handling chains and
2773/// ensures rich diagnostic information is preserved.
2774///
2775/// # Type Parameters
2776///
2777/// * `T` - The `Ok` type of the `Result`.
2778///
2779/// # Examples
2780///
2781/// ```
2782/// use yoshi_std::{Yoshi, YoshiKind, HatchExt};
2783/// # use std::io;
2784/// # use std::io::ErrorKind;
2785///
2786/// fn process_data(input: &str) -> Result<usize, Yoshi> {
2787/// if input.is_empty() {
2788/// return Err(Yoshi::new(YoshiKind::Validation {
2789/// field: "input".into(),
2790/// message: "Input cannot be empty".into(),
2791/// expected: Some("non-empty string".into()),
2792/// actual: Some("empty string".into()),
2793/// }))
2794/// .context("Failed to validate data")
2795/// .with_suggestion("Provide non-empty input");
2796/// }
2797///
2798/// // Simulate an I/O operation that might fail
2799/// let result: std::result::Result<usize, io::Error> = Err(io::Error::new(ErrorKind::Other, "disk full"));
2800///
2801/// result
2802/// .map_err(Yoshi::from) // Convert io::Error to Yoshi
2803/// .context("Failed to write processed data to disk")
2804/// .map_err(|e| e.with_metadata("file_size", "10MB").with_priority(200))
2805/// }
2806///
2807/// # fn main() {
2808/// let result = process_data("");
2809/// assert!(result.is_err());
2810/// let error = result.unwrap_err();
2811/// println!("Error: {}", error);
2812/// assert!(error.to_string().contains("Input cannot be empty"));
2813///
2814/// let result2 = process_data("some_data");
2815/// if let Err(e) = result2 {
2816/// println!("Error: {}", e);
2817/// assert!(e.to_string().contains("Failed to write processed data to disk"));
2818/// assert_eq!(e.primary_context().unwrap().metadata.get("file_size".into()).map(|s| s.as_ref()), Some("10MB"));
2819/// assert_eq!(e.primary_context().unwrap().priority, 200);
2820/// }
2821/// # }
2822/// ```
2823pub trait HatchExt<T>
2824where
2825 Self: Sized,
2826{
2827 /// Adds a context message to the error.
2828 ///
2829 /// If `self` is `Ok`, it is returned unchanged. If `self` is `Err`, its error
2830 /// is converted to a `Yoshi` error if it isn't already, and a new `YoContext`
2831 /// with the provided message is added to it.
2832 ///
2833 /// This method preserves the current source code location (file, line, column).
2834 ///
2835 /// # Arguments
2836 ///
2837 /// * `msg` - The context message.
2838 ///
2839 /// # Returns
2840 ///
2841 /// A `Result<T, Yoshi>` with the added context on error.
2842 ///
2843 /// # Examples
2844 ///
2845 /// ```
2846 /// use yoshi_std::{Yoshi, HatchExt};
2847 /// # use std::io;
2848 /// # use std::io::ErrorKind;
2849 ///
2850 /// fn read_file(path: &str) -> Result<String, Yoshi> {
2851 /// std::fs::read_to_string(path)
2852 /// .map_err(Yoshi::from)
2853 /// .context(format!("Failed to read file: {}", path))
2854 /// }
2855 ///
2856 /// # fn main() {
2857 /// let result = read_file("non_existent_file.txt");
2858 /// if let Err(e) = result {
2859 /// println!("Error: {}", e);
2860 /// assert!(e.to_string().contains("Failed to read file: non_existent_file.txt"));
2861 /// }
2862 /// # }
2863 /// ```
2864 ///
2865 /// # Errors
2866 ///
2867 /// Returns a `Yoshi` error with added context if the result is an error.
2868 #[track_caller]
2869 fn context(self, msg: impl Into<String>) -> Result<T>;
2870
2871 /// Adds a suggestion to the error's primary context.
2872 ///
2873 /// If `self` is `Ok`, it is returned unchanged. If `self` is `Err`, its error
2874 /// is converted to a `Yoshi` error if it isn't already, and a new suggestion
2875 /// is added to its primary `YoContext`.
2876 ///
2877 /// # Arguments
2878 ///
2879 /// * `s` - The suggestion message.
2880 ///
2881 /// # Returns
2882 ///
2883 /// A `Result<T, Yoshi>` with the added suggestion on error.
2884 ///
2885 /// # Examples
2886 ///
2887 /// ```
2888 /// use yoshi_std::{Yoshi, HatchExt};
2889 /// # use std::io;
2890 /// # use std::io::ErrorKind;
2891 ///
2892 /// fn connect_db() -> Result<(), Yoshi> {
2893 /// // Simulate a connection error
2894 /// Err(io::Error::new(ErrorKind::ConnectionRefused, "db connection refused"))
2895 /// .map_err(Yoshi::from)
2896 /// .with_suggestion("Ensure the database server is running.")
2897 /// }
2898 /// # fn main() {
2899 /// let result = connect_db();
2900 /// if let Err(e) = result {
2901 /// println!("Error: {}", e);
2902 /// assert!(e.suggestion().as_deref() == Some("Ensure the database server is running."));
2903 /// }
2904 /// # }
2905 /// ```
2906 ///
2907 /// # Errors
2908 ///
2909 /// Returns a `Yoshi` error with added suggestion if the result is an error.
2910 #[track_caller]
2911 fn with_suggestion(self, s: impl Into<String>) -> Result<T>;
2912
2913 /// Attaches a typed shell to the error's primary context.
2914 ///
2915 /// If `self` is `Ok`, it is returned unchanged. If `self` is `Err`, its error
2916 /// is converted to a `Yoshi` error if it isn't already, and a new typed shell
2917 /// is added to its primary `YoContext`.
2918 ///
2919 /// # Arguments
2920 ///
2921 /// * `p` - The shell to attach. Must be `Any + Send + Sync + 'static`.
2922 ///
2923 /// # Returns
2924 ///
2925 /// A `Result<T, Yoshi>` with the added shell on error.
2926 /// /// # Examples
2927 ///
2928 /// ```
2929 /// use yoshi_std::{Yoshi, YoshiKind, HatchExt};
2930 /// # use std::io;
2931 /// # use std::io::ErrorKind;
2932 ///
2933 /// #[derive(Debug, PartialEq)]
2934 /// struct RequestInfo {
2935 /// request_id: String,
2936 /// user_agent: String,
2937 /// }
2938 ///
2939 /// fn process_request(id: &str, ua: &str) -> Result<(), Yoshi> {
2940 /// // Simulate an internal error
2941 /// Err(Yoshi::new(YoshiKind::Internal {
2942 /// message: "Processing failed".into(),
2943 /// source: None,
2944 /// component: None,
2945 /// }))
2946 /// .with_shell(RequestInfo { request_id: id.into(), user_agent: ua.into() })
2947 /// }
2948 ///
2949 /// # fn main() {
2950 /// let result = process_request("req123", "Mozilla/5.0");
2951 /// if let Err(e) = result {
2952 /// println!("Error: {}", e);
2953 /// let info = e.shell::<RequestInfo>();
2954 /// assert!(info.is_some());
2955 /// assert_eq!(info.unwrap().request_id, "req123");
2956 /// }
2957 /// # }
2958 /// ```
2959 ///
2960 /// # Errors
2961 ///
2962 /// Returns a `Yoshi` error with added shell if the result is an error.
2963 #[track_caller]
2964 fn with_shell(self, p: impl Any + Send + Sync + 'static) -> Result<T>;
2965
2966 /// Sets the priority for the error's primary context.
2967 ///
2968 /// If `self` is `Ok`, it is returned unchanged. If `self` is `Err`, its error
2969 /// is converted to a `Yoshi` error if it isn't already, and the priority of its
2970 /// primary `YoContext` is updated.
2971 ///
2972 /// # Arguments
2973 ///
2974 /// * `priority` - The priority level (0-255).
2975 ///
2976 /// # Returns
2977 ///
2978 /// A `Result<T, Yoshi>` with the updated priority on error.
2979 ///
2980 /// # Examples
2981 ///
2982 /// ```
2983 /// use yoshi_std::{Yoshi, YoshiKind, HatchExt};
2984 ///
2985 /// fn perform_critical_op() -> Result<(), Yoshi> {
2986 /// // Simulate a critical error
2987 /// Err(Yoshi::new(YoshiKind::Internal {
2988 /// message: "Critical operation failed".into(),
2989 /// source: None,
2990 /// component: None,
2991 /// }))
2992 /// .with_priority(250) // Mark as very high priority
2993 /// }
2994 /// # fn main() {
2995 /// let result = perform_critical_op();
2996 /// if let Err(e) = result {
2997 /// println!("Error: {}", e);
2998 /// assert_eq!(e.primary_context().unwrap().priority, 250);
2999 /// }
3000 /// # }
3001 /// ```
3002 ///
3003 /// # Errors
3004 ///
3005 /// Returns a `Yoshi` error with updated priority if the result is an error.
3006 #[track_caller]
3007 fn with_priority(self, priority: u8) -> Result<T>;
3008 /// Short alias for `context`.
3009 ///
3010 /// # Errors
3011 ///
3012 /// Returns a `Yoshi` error with added context if the result is an error.
3013 #[track_caller]
3014 fn ctx(self, msg: impl Into<String>) -> Result<T>;
3015
3016 /// Short alias for `with_suggestion`.
3017 ///
3018 /// # Errors
3019 ///
3020 /// Returns a `Yoshi` error with added suggestion if the result is an error.
3021 #[track_caller]
3022 fn help(self, s: impl Into<String>) -> Result<T>;
3023
3024 /// Adds metadata to the error's primary context.
3025 ///
3026 /// This is a convenience method that delegates to `Yoshi::with_metadata`.
3027 ///
3028 /// # Arguments
3029 ///
3030 /// * `k` - The metadata key.
3031 /// * `v` - The metadata value.
3032 ///
3033 /// # Returns
3034 ///
3035 /// A `Result<T, Yoshi>` with the added metadata on error.
3036 ///
3037 /// # Examples
3038 ///
3039 /// ```
3040 /// use yoshi_std::{Yoshi, YoshiKind, HatchExt, Arc};
3041 ///
3042 /// fn fetch_user_data() -> Result<String, Yoshi> {
3043 /// // Simulate an error during user data fetch
3044 /// Err(Yoshi::new(YoshiKind::NotFound {
3045 /// resource_type: "User".into(),
3046 /// identifier: "unknown_user".into(),
3047 /// search_locations: None,
3048 /// }))
3049 /// .meta("user_id", "12345")
3050 /// .meta("trace_id", "abcde-12345")
3051 /// }
3052 ///
3053 /// # fn main() {
3054 /// let result = fetch_user_data();
3055 /// if let Err(e) = result {
3056 /// println!("Error: {}", e);
3057 /// let metadata = e.primary_context().unwrap().metadata.clone();
3058 /// assert_eq!(metadata.get(&Arc::from("user_id")).map(|s| s.as_ref()), Some("12345"));
3059 /// }
3060 /// # }
3061 /// ```
3062 /// # Errors
3063 ///
3064 /// Returns a `Yoshi` error with added metadata if the result is an error.
3065 #[track_caller]
3066 fn meta(self, k: impl Into<String>, v: impl Into<String>) -> Result<T>;
3067}
3068
3069//--------------------------------------------------------------------------------------------------
3070// Enhanced Yoshi main error type with performance optimization
3071//--------------------------------------------------------------------------------------------------
3072
3073/// The main `Yoshi` error type with enterprise-grade performance optimization.
3074///
3075/// `Yoshi` is a highly structured and extensible error type designed for
3076/// complex applications. It combines a categorized error kind, contextual
3077/// information, and optional backtrace capture into a single, cohesive unit.
3078///
3079/// # Fields
3080///
3081/// - `kind`: The primary classification of the error, provided by [`YoshiKind`].
3082/// - `backtrace`: An optional [`YoshiBacktrace`] providing stack trace information (only with `std` feature).
3083/// - `contexts`: A vector of [`YoContext`] instances, providing additional
3084/// details and context about the error's propagation.
3085/// - `instance_id`: A unique identifier for each `Yoshi` error instance.
3086/// - `created_at`: The `SystemTime` when the error was created (only with `std` feature).
3087///
3088/// # Examples
3089///
3090/// Basic error creation:
3091/// ```
3092/// use yoshi_std::{Yoshi, YoshiKind};
3093///
3094/// let err = Yoshi::new(YoshiKind::Internal {
3095/// message: "Something went wrong internally".into(),
3096/// source: None,
3097/// component: None,
3098/// });
3099///
3100/// println!("Error: {}", err);
3101/// ```
3102///
3103/// Creating an error with context:
3104/// ```
3105/// use yoshi_std::{Yoshi, YoshiKind, HatchExt};
3106/// # use std::io::{self, ErrorKind};
3107///
3108/// fn load_data() -> Result<(), Yoshi> {
3109/// // Simulate a file not found error
3110/// let io_error = io::Error::new(ErrorKind::NotFound, "data.json not found");
3111/// let error = Yoshi::from(io_error)
3112/// .context("Failed to load user preferences".to_string())
3113/// .with_metadata("user_id", "test_user")
3114/// .with_suggestion("Ensure data.json is in the correct directory.");
3115/// Err(error)
3116/// }
3117///
3118/// # fn main() {
3119/// match load_data() {
3120/// Ok(_) => println!("Data loaded successfully"),
3121/// Err(error) => eprintln!("Error: {}", error),
3122/// }
3123/// # }
3124/// ```
3125#[derive(Debug)]
3126pub struct Yoshi {
3127 /// The underlying error kind.
3128 kind: YoshiKind,
3129 /// Optional backtrace for debugging and performance metadata (only available with `std` feature).
3130 #[cfg(feature = "std")]
3131 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
3132 backtrace: Option<YoshiBacktrace>,
3133 /// Placeholder for backtrace when `std` feature is not enabled.
3134 #[cfg(not(feature = "std"))]
3135 #[cfg_attr(docsrs, doc(cfg(not(feature = "std"))))]
3136 backtrace: Option<()>,
3137 /// Contexts providing additional information about the error.
3138 contexts: Vec<YoContext>,
3139 /// A unique identifier for this error instance.
3140 instance_id: u32,
3141 /// Timestamp when the error was created (only available with `std` feature).
3142 #[cfg(feature = "std")]
3143 #[allow(dead_code)]
3144 created_at: SystemTime,
3145}
3146
3147impl Clone for Yoshi {
3148 /// Creates a clone of the `Yoshi` error.
3149 ///
3150 /// Note: In `std` mode, the backtrace is not cloned (as `std::backtrace::Backtrace`
3151 /// doesn't implement `Clone`). Instead, the clone will have no backtrace (`None`).
3152 /// In `no_std` mode, the backtrace is properly cloned as it only contains basic
3153 /// location information.
3154 ///
3155 /// A new unique instance ID is generated for the clone to maintain error tracking.
3156 fn clone(&self) -> Self {
3157 let instance_id = ERROR_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
3158
3159 Self {
3160 kind: self.kind.clone(),
3161 #[cfg(feature = "std")]
3162 backtrace: None, // Cannot clone std::backtrace::Backtrace, so set to None
3163 #[cfg(not(feature = "std"))]
3164 backtrace: self.backtrace.clone(), // YoshiBacktrace implements Clone in no_std mode
3165 contexts: self.contexts.clone(),
3166 instance_id,
3167 #[cfg(feature = "std")]
3168 created_at: SystemTime::now(), // Use current time for the clone
3169 }
3170 }
3171}
3172
3173impl Yoshi {
3174 /// Creates a new `Yoshi` error with optimized allocation and monitoring.
3175 ///
3176 /// This is the primary constructor for `Yoshi` errors. It increments
3177 /// a global instance counter and, if the `std` feature is enabled and
3178 /// backtraces are enabled via environment variables (`RUST_BACKTRACE`
3179 /// or `RUST_LIB_BACKTRACE`), it captures a backtrace.
3180 ///
3181 /// # Arguments
3182 ///
3183 /// * `kind` - The [`YoshiKind`] that categorizes this error.
3184 ///
3185 /// # Returns
3186 ///
3187 /// A new `Yoshi` error instance.
3188 ///
3189 /// # Examples
3190 ///
3191 /// ```
3192 /// use yoshi_std::{Yoshi, YoshiKind};
3193 ///
3194 /// let err = Yoshi::new(YoshiKind::NotFound {
3195 /// resource_type: "User".into(),
3196 /// identifier: "john.doe".into(),
3197 /// search_locations: None,
3198 /// });
3199 ///
3200 /// assert!(matches!(err.kind(), YoshiKind::NotFound { .. }));
3201 /// ```
3202 #[inline]
3203 pub fn new(kind: YoshiKind) -> Self {
3204 let instance_id = ERROR_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
3205
3206 Self {
3207 kind,
3208 #[cfg(feature = "std")]
3209 backtrace: capture_bt(),
3210 #[cfg(not(feature = "std"))]
3211 backtrace: None,
3212 contexts: Vec::with_capacity(4), // Pre-allocate for typical error chain depth
3213 instance_id,
3214 #[cfg(feature = "std")]
3215 created_at: SystemTime::now(),
3216 }
3217 }
3218
3219 /// Creates a new `Yoshi` error by wrapping a foreign `Error` trait object.
3220 ///
3221 /// This is an explicit conversion for generic error types, allowing them
3222 /// to be integrated into the `Yoshi` error chain without requiring a
3223 /// blanket `From` implementation that might conflict or cause issues
3224 /// with unstable features.
3225 /// The type name of the wrapped error is captured for diagnostic purposes.
3226 ///
3227 /// # Type Parameters
3228 ///
3229 /// * `E` - The type of the foreign error, which must implement `Error`,
3230 /// `Send`, `Sync`, and have a `'static` lifetime.
3231 ///
3232 /// # Arguments
3233 ///
3234 /// * `e` - The foreign error instance to wrap.
3235 ///
3236 /// # Returns
3237 ///
3238 /// A new `Yoshi` error with its kind to `YoshiKind::Foreign`.
3239 ///
3240 /// # Examples
3241 ///
3242 /// ```
3243 /// use std::io;
3244 /// use yoshi_std::{Yoshi, YoshiKind};
3245 ///
3246 /// #[derive(Debug)]
3247 /// struct CustomError;
3248 /// impl std::fmt::Display for CustomError {
3249 /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3250 /// write!(f, "a custom error occurred")
3251 /// }
3252 /// }
3253 /// impl std::error::Error for CustomError {}
3254 ///
3255 /// let io_error = io::Error::new(io::ErrorKind::Other, "disk full");
3256 /// let yoshi_io_error = Yoshi::foreign(io_error);
3257 /// assert!(matches!(yoshi_io_error.kind(), YoshiKind::Foreign { .. }));
3258 /// println!("Wrapped IO error: {}", yoshi_io_error);
3259 ///
3260 /// let custom_error = CustomError;
3261 /// let yoshi_custom_error = Yoshi::foreign(custom_error);
3262 /// assert!(matches!(yoshi_custom_error.kind(), YoshiKind::Foreign { .. }));
3263 /// println!("Wrapped custom error: {}", yoshi_custom_error);
3264 /// ```
3265 #[inline]
3266 #[track_caller]
3267 pub fn foreign<E>(e: E) -> Self
3268 where
3269 E: Error + Send + Sync + 'static,
3270 {
3271 Self::new(YoshiKind::from_foreign_with_context(e, ""))
3272 }
3273
3274 /// Gets the unique instance ID for debugging and correlation.
3275 ///
3276 /// Each `Yoshi` error instance is assigned a unique `u64` ID upon creation.
3277 /// This ID can be used to track specific error occurrences in logs or
3278 /// telemetry systems, especially in highly concurrent environments.
3279 ///
3280 /// # Returns
3281 ///
3282 /// The unique instance ID of this `Yoshi` error.
3283 ///
3284 /// # Examples
3285 ///
3286 /// ```
3287 /// # use yoshi_std::{Yoshi, YoshiKind};
3288 /// let err1 = Yoshi::new(YoshiKind::Internal {
3289 /// message: "test".into(),
3290 /// source: None,
3291 /// component: None,
3292 /// });
3293 /// let err2 = Yoshi::new(YoshiKind::Internal {
3294 /// message: "test".into(),
3295 /// source: None,
3296 /// component: None,
3297 /// });
3298 ///
3299 /// assert_ne!(err1.instance_id(), err2.instance_id());
3300 /// println!("Error 1 ID: {}", err1.instance_id());
3301 /// println!("Error 2 ID: {}", err2.instance_id());
3302 /// ```
3303 #[inline]
3304 pub const fn instance_id(&self) -> u32 {
3305 self.instance_id
3306 }
3307
3308 /// Returns a reference to the `YoshiKind` of this error.
3309 ///
3310 /// This allows inspecting the high-level classification of the error
3311 /// and accessing its specific fields.
3312 ///
3313 /// # Returns
3314 ///
3315 /// A constant reference to the [`YoshiKind`] enum variant.
3316 ///
3317 /// # Examples
3318 ///
3319 /// ```
3320 /// use yoshi_std::{Yoshi, YoshiKind};
3321 ///
3322 /// let err = Yoshi::new(YoshiKind::NotFound {
3323 /// resource_type: "User".into(),
3324 /// identifier: "john.doe".into(),
3325 /// search_locations: None,
3326 /// });
3327 ///
3328 /// match err.kind() {
3329 /// YoshiKind::NotFound { identifier, .. } => {
3330 /// println!("User not found: {}", identifier);
3331 /// }
3332 /// _ => (),
3333 /// }
3334 /// ```
3335 #[inline]
3336 pub const fn kind(&self) -> &YoshiKind {
3337 &self.kind
3338 }
3339
3340 /// Gets the error severity level (0-100).
3341 ///
3342 /// This is a convenience method that delegates to `self.kind().severity()`.
3343 ///
3344 /// # Returns
3345 ///
3346 /// A `u8` value indicating the severity of the error.
3347 ///
3348 /// # Examples
3349 ///
3350 /// ```
3351 /// # use yoshi_std::YoshiKind;
3352 /// let internal_error = YoshiKind::Internal {
3353 /// message: "simulated error".into(),
3354 /// source: None,
3355 /// component: None,
3356 /// };
3357 /// assert_eq!(internal_error.severity(), 80);
3358 ///
3359 /// let validation_error = YoshiKind::Validation {
3360 /// field: "email".into(),
3361 /// message: "Invalid format".into(),
3362 /// expected: None,
3363 /// actual: None,
3364 /// };
3365 /// assert_eq!(validation_error.severity(), 20);
3366 /// ```
3367 #[inline]
3368 pub const fn severity(&self) -> u8 {
3369 self.kind.severity()
3370 }
3371
3372 /// Checks if this is a transient error that might succeed on retry.
3373 ///
3374 /// This is a convenience method that delegates to `self.kind().is_transient()`.
3375 ///
3376 /// # Returns
3377 ///
3378 /// `true` if the error's kind is considered transient, `false` otherwise.
3379 ///
3380 /// # Examples
3381 ///
3382 /// ```
3383 /// # use yoshi_std::YoshiKind;
3384 /// # use core::time::Duration;
3385 /// let timeout_error = YoshiKind::Timeout {
3386 /// operation: "API call".into(),
3387 /// duration: Duration::from_secs(10),
3388 /// expected_max: None,
3389 /// };
3390 /// assert!(timeout_error.is_transient());
3391 ///
3392 /// let config_error = YoshiKind::Config {
3393 /// message: "Missing key".into(),
3394 /// source: None,
3395 /// config_path: None,
3396 /// };
3397 /// assert!(!config_error.is_transient());
3398 /// ```
3399 #[inline]
3400 pub const fn is_transient(&self) -> bool {
3401 self.kind.is_transient()
3402 }
3403
3404 /// Adds a context message to the error.
3405 ///
3406 /// This method enhances the error with additional diagnostic information,
3407 /// making it easier to trace the origin and propagation of failures.
3408 ///
3409 /// # Arguments
3410 ///
3411 /// * `msg` - The context message. It can be any type that converts into a `String`.
3412 ///
3413 /// # Returns
3414 ///
3415 /// The modified `Yoshi` error instance with the new context.
3416 ///
3417 /// # Examples
3418 ///
3419 /// ```
3420 /// use yoshi_std::{Yoshi, YoshiKind};
3421 ///
3422 /// let err = Yoshi::new(YoshiKind::Internal {
3423 /// message: "database query failed".into(),
3424 /// source: None,
3425 /// component: None,
3426 /// })
3427 /// .context("Attempting to fetch user data");
3428 ///
3429 /// println!("Error: {}", err);
3430 /// ```
3431 ///
3432 /// # Panics
3433 ///
3434 /// This method may panic if context storage fails, though this is extremely unlikely.
3435 #[track_caller]
3436 #[inline]
3437 #[must_use]
3438 pub fn context(mut self, msg: impl Into<String>) -> Self {
3439 self.contexts
3440 .push(YoContext::new(msg).with_location(yoshi_location!()));
3441 self
3442 }
3443
3444 /// Adds a suggestion to the error's primary context.
3445 ///
3446 /// This method adds a human-readable suggestion to the current `Yoshi` error.
3447 /// The suggestion is stored in the primary (most recent) context associated
3448 /// with this error.
3449 ///
3450 /// # Arguments
3451 ///
3452 /// * `s` - The suggestion message. It can be any type that converts into a `String`.
3453 ///
3454 /// # Returns
3455 ///
3456 /// The modified `Yoshi` error instance with the new suggestion.
3457 ///
3458 /// # Examples
3459 ///
3460 /// ```
3461 /// use yoshi_std::{Yoshi, YoshiKind};
3462 /// /// let err = Yoshi::new(YoshiKind::Io(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "file access denied")))
3463 /// .with_suggestion("Check file permissions or path.");
3464 ///
3465 /// assert!(err.suggestion().as_deref() == Some("Check file permissions or path."));
3466 /// ```
3467 ///
3468 /// # Panics
3469 ///
3470 /// This method may panic if the context storage fails, though this is extremely unlikely.
3471 #[inline]
3472 #[track_caller]
3473 #[must_use]
3474 pub fn with_suggestion(mut self, s: impl Into<String>) -> Self {
3475 // Ensure there's at least one context to attach the suggestion to
3476 if self.contexts.is_empty() {
3477 self.contexts
3478 .push(YoContext::new("Error occurred").with_location(yoshi_location!()));
3479 }
3480 self.contexts
3481 .last_mut()
3482 .expect("contexts should not be empty")
3483 .suggestion = Some(intern_string(s.into()));
3484 self
3485 }
3486
3487 /// Attaches a component identifier to the error's primary context.
3488 ///
3489 /// This method adds a component identifier to help categorize and trace
3490 /// errors within different parts of a system or application. The component
3491 /// information is stored as metadata with the key "component".
3492 ///
3493 /// # Arguments
3494 ///
3495 /// * `component` - The component identifier. It can be any type that converts into a `String`.
3496 ///
3497 /// # Returns
3498 ///
3499 /// The modified `Yoshi` error instance with the component information.
3500 ///
3501 /// # Examples
3502 ///
3503 /// ```
3504 /// use yoshi_std::{Yoshi, YoshiKind};
3505 ///
3506 /// let err = Yoshi::new(YoshiKind::Internal {
3507 /// message: "operation failed".into(),
3508 /// source: None,
3509 /// component: None,
3510 /// })
3511 /// .with_component("database");
3512 ///
3513 /// // Component can be retrieved from metadata
3514 /// let ctx = err.primary_context().unwrap();
3515 /// assert_eq!(ctx.metadata.get("component").map(|s| s.as_ref()), Some("database"));
3516 /// ```
3517 ///
3518 /// # Panics
3519 ///
3520 /// This method may panic if the context storage fails, though this is extremely unlikely.
3521 #[inline]
3522 #[track_caller]
3523 #[must_use]
3524 pub fn with_component(mut self, component: impl Into<String>) -> Self {
3525 // Ensure there's at least one context to attach the component to
3526 if self.contexts.is_empty() {
3527 self.contexts
3528 .push(YoContext::new("Error occurred").with_location(yoshi_location!()));
3529 }
3530 self.contexts
3531 .last_mut()
3532 .expect("contexts should not be empty")
3533 .metadata
3534 .insert(intern_string("component"), intern_string(component.into()));
3535 self
3536 }
3537
3538 /// Attaches a typed shell to the error's primary context.
3539 ///
3540 /// This method allows embedding arbitrary Rust types within the error's context.
3541 /// This is useful for passing structured, type-safe debugging information
3542 /// that can be retrieved later using `shell::<T>()`.
3543 ///
3544 /// # Arguments
3545 ///
3546 /// * `shell` - The data to attach. It must implement `Any`, `Send`, `Sync`, and have a `'static` lifetime.
3547 ///
3548 /// # Returns
3549 ///
3550 /// The modified `Yoshi` error instance with the new shell.
3551 ///
3552 /// # Examples
3553 ///
3554 /// ```
3555 /// use yoshi_std::{Yoshi, YoshiKind};
3556 ///
3557 /// #[derive(Debug, PartialEq)]
3558 /// struct RequestContext {
3559 /// user_id: u64,
3560 /// request_path: String,
3561 /// }
3562 ///
3563 /// let err = Yoshi::new(YoshiKind::Internal {
3564 /// message: "handler failed".into(),
3565 /// source: None,
3566 /// component: None,
3567 /// })
3568 /// .with_shell(RequestContext { user_id: 123, request_path: "/api/data".to_string() });
3569 ///
3570 /// let ctx_payload = err.shell::<RequestContext>().unwrap();
3571 /// assert_eq!(ctx_payload.user_id, 123);
3572 /// ```
3573 ///
3574 /// # Panics
3575 ///
3576 /// This method may panic if the shell storage fails, though this is extremely unlikely.
3577 #[inline]
3578 #[track_caller]
3579 #[must_use]
3580 pub fn with_shell(mut self, shell: impl Any + Send + Sync + 'static) -> Self {
3581 // Ensure there's at least one context to attach the shell to
3582 if self.contexts.is_empty() {
3583 self.contexts
3584 .push(YoContext::new("Error occurred").with_location(yoshi_location!()));
3585 }
3586 self.contexts
3587 .last_mut()
3588 .expect("contexts should not be empty")
3589 .add_shell_in_place(shell);
3590 self
3591 }
3592
3593 /// Sets the priority for the error's primary context.
3594 ///
3595 /// Priority can be used to indicate the relative importance of a context
3596 /// message, influencing how errors are logged or processed by error handling
3597 /// systems. Higher values indicate higher priority.
3598 ///
3599 /// # Arguments
3600 ///
3601 /// * `priority` - The priority level (0-255).
3602 ///
3603 /// # Returns
3604 ///
3605 /// The modified `Yoshi` error instance with the updated priority.
3606 ///
3607 /// # Examples
3608 /// ```
3609 /// use yoshi_std::{Yoshi, YoshiKind};
3610 ///
3611 /// let err = Yoshi::new(YoshiKind::Internal {
3612 /// message: "critical failure".into(),
3613 /// source: None,
3614 /// component: None,
3615 /// })
3616 /// .with_priority(250); // Highest priority
3617 ///
3618 /// assert_eq!(err.primary_context().unwrap().priority, 250);
3619 /// ```
3620 ///
3621 /// # Panics
3622 ///
3623 /// This method ensures that there is at least one context before updating priority.
3624 /// If no contexts exist, it creates one automatically, so this method should not panic.
3625 #[inline]
3626 #[must_use]
3627 #[track_caller]
3628 pub fn with_priority(mut self, priority: u8) -> Self {
3629 // Ensure there's at least one context to update
3630 if self.contexts.is_empty() {
3631 self.contexts
3632 .push(YoContext::new("Error occurred").with_location(yoshi_location!()));
3633 }
3634 self.contexts
3635 .last_mut()
3636 .expect("contexts should not be empty")
3637 .priority = priority;
3638 self
3639 }
3640
3641 /// Adds metadata to the error's primary context.
3642 ///
3643 /// Metadata are key-value pairs that provide additional, unstructured
3644 /// diagnostic information. These can be used for logging, filtering,
3645 /// or passing arbitrary data alongside the error.
3646 ///
3647 /// # Arguments
3648 ///
3649 /// * `k` - The metadata key. It can be any type that converts into a `String`.
3650 /// * `v` - The metadata value. It can be any type that converts into a `String`.
3651 ///
3652 /// # Returns
3653 ///
3654 /// The modified `Yoshi` error instance with the new metadata.
3655 ///
3656 /// # Examples
3657 ///
3658 /// ```
3659 /// use yoshi_std::{Yoshi, YoshiKind, Arc};
3660 ///
3661 /// let err = Yoshi::new(YoshiKind::Internal {
3662 /// message: "cache read failed".into(),
3663 /// source: None,
3664 /// component: None,
3665 /// })
3666 /// .with_metadata("cache_key", "user_profile_123")
3667 /// .with_metadata("region", "us-east-1");
3668 ///
3669 /// let metadata = &err.primary_context().unwrap().metadata;
3670 /// assert_eq!(metadata.get(&Arc::from("cache_key")).map(|s| s.as_ref()), Some("user_profile_123"));
3671 /// assert_eq!(metadata.get(&Arc::from("region")).map(|s| s.as_ref()), Some("us-east-1"));
3672 /// ```
3673 ///
3674 /// # Panics
3675 ///
3676 /// This method may panic if metadata storage fails, though this is extremely unlikely.
3677 #[inline]
3678 #[track_caller]
3679 #[must_use]
3680 pub fn with_metadata(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
3681 // Ensure there's at least one context to attach metadata to
3682 if self.contexts.is_empty() {
3683 self.contexts
3684 .push(YoContext::new("Error occurred").with_location(yoshi_location!()));
3685 }
3686 self.contexts
3687 .last_mut()
3688 .expect("contexts should not be empty")
3689 .metadata
3690 .insert(intern_string(k.into()), intern_string(v.into()));
3691 self
3692 }
3693
3694 /// Sets location information on the error's primary context.
3695 ///
3696 /// This method attaches source code location information to the error's primary context,
3697 /// helping with debugging and error tracing. It consumes `self` and returns a modified `Self`.
3698 ///
3699 /// # Arguments
3700 ///
3701 /// * `location` - The `YoshiLocation` to set.
3702 ///
3703 /// # Returns
3704 ///
3705 /// The modified `Yoshi` error instance with the location set.
3706 ///
3707 /// # Examples
3708 ///
3709 /// ```
3710 /// use yoshi_std::{Yoshi, YoshiKind, YoshiLocation};
3711 ///
3712 /// let location = YoshiLocation::new("src/main.rs", 10, 5);
3713 /// let err = Yoshi::new(YoshiKind::Internal {
3714 /// message: "operation failed".into(),
3715 /// source: None,
3716 /// component: None,
3717 /// })
3718 /// .with_location(location);
3719 ///
3720 /// assert_eq!(err.primary_context().unwrap().location.unwrap().file, "src/main.rs");
3721 /// assert_eq!(err.primary_context().unwrap().location.unwrap().line, 10);
3722 /// ```
3723 ///
3724 /// # Panics
3725 ///
3726 /// This method may panic if location storage fails, though this is extremely unlikely.
3727 #[inline]
3728 #[track_caller]
3729 #[must_use]
3730 pub fn with_location(mut self, location: YoshiLocation) -> Self {
3731 // Ensure there's at least one context to attach location to
3732 if self.contexts.is_empty() {
3733 self.contexts
3734 .push(YoContext::new("Error occurred").with_location(yoshi_location!()));
3735 }
3736 self.contexts
3737 .last_mut()
3738 .expect("contexts should not be empty")
3739 .location = Some(location);
3740 self
3741 }
3742
3743 /// Returns a reference to the optional backtrace.
3744 ///
3745 /// The backtrace is only available when the `std` feature is enabled and
3746 /// `RUST_BACKTRACE` or `RUST_LIB_BACKTRACE` environment variables are set.
3747 ///
3748 /// # Returns
3749 ///
3750 /// An `Option` containing a reference to the [`YoshiBacktrace`] if available,
3751 /// otherwise `None`.
3752 ///
3753 /// # Examples
3754 ///
3755 /// ```no_run
3756 /// # #[cfg(feature = "std")] {
3757 /// # use yoshi_std::{Yoshi, YoshiKind};
3758 /// # std::env::set_var("RUST_BACKTRACE", "1");
3759 /// let err = Yoshi::new(YoshiKind::Internal {
3760 /// message: "test error".into(),
3761 /// source: None,
3762 /// component: None,
3763 /// });
3764 /// if let Some(bt) = err.backtrace() {
3765 /// println!("Backtrace: {}", bt);
3766 /// }
3767 /// # }
3768 /// ```
3769 #[cfg(feature = "std")]
3770 #[inline]
3771 pub const fn backtrace(&self) -> Option<&YoshiBacktrace> {
3772 self.backtrace.as_ref()
3773 }
3774
3775 /// Returns a reference to the underlying foreign error (if `YoshiKind::Foreign`).
3776 ///
3777 /// This method allows downcasting the boxed `dyn Error` contained within a
3778 /// `YoshiKind::Foreign` variant to a concrete type.
3779 ///
3780 /// # Type Parameters
3781 ///
3782 /// * `T` - The concrete type to downcast to, which must implement `Error`.
3783 ///
3784 /// # Returns
3785 ///
3786 /// An `Option` containing a reference to the downcasted error of type `T`,
3787 /// or `None` if the error is not `YoshiKind::Foreign` or cannot be downcasted
3788 /// to the specified type.
3789 /// /// # Examples
3790 ///
3791 /// ```
3792 /// use std::io;
3793 /// use yoshi_std::{Yoshi, YoshiKind};
3794 ///
3795 /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file.txt not found");
3796 /// let yoshi_err = Yoshi::foreign(io_err);
3797 ///
3798 /// // Attempt to downcast to io::Error
3799 /// if let Some(err) = yoshi_err.downcast_ref::<io::Error>() {
3800 /// assert_eq!(err.kind(), io::ErrorKind::NotFound);
3801 /// } else {
3802 /// panic!("Expected io::Error");
3803 /// }
3804 /// ```
3805 #[inline]
3806 pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> {
3807 if let YoshiKind::Foreign { error, .. } = &self.kind {
3808 // First try to downcast the ForeignErrorWrapper itself to T
3809 if let Some(result) = error.downcast_ref::<T>() {
3810 return Some(result);
3811 }
3812
3813 // If that fails, try to downcast the wrapper to ForeignErrorWrapper
3814 // and then downcast its inner error to T
3815 if let Some(wrapper) = error.downcast_ref::<ForeignErrorWrapper>() {
3816 wrapper.inner.downcast_ref::<T>()
3817 } else {
3818 None
3819 }
3820 } else {
3821 None
3822 }
3823 }
3824
3825 /// Returns a mutable reference to the underlying foreign error (if `YoshiKind::Foreign`).
3826 ///
3827 /// This method allows mutable downcasting the boxed `dyn Error` contained within a
3828 /// `YoshiKind::Foreign` variant to a concrete type.
3829 ///
3830 /// # Type Parameters
3831 ///
3832 /// * `T` - The concrete type to downcast to, which must implement `Error`.
3833 ///
3834 /// # Returns
3835 /// /// An `Option` containing a mutable reference to the downcasted error of type `T`,
3836 /// or `None` if the error is not `YoshiKind::Foreign` or cannot be downcasted
3837 /// to the specified type.
3838 #[inline]
3839 pub fn downcast_mut<T: Error + 'static>(&mut self) -> Option<&mut T> {
3840 if let YoshiKind::Foreign { error, .. } = &mut self.kind {
3841 // Use a single downcast operation and then check both possibilities
3842 if error.is::<ForeignErrorWrapper>() {
3843 // If it's a ForeignErrorWrapper, get the inner error
3844 if let Some(wrapper) = error.downcast_mut::<ForeignErrorWrapper>() {
3845 wrapper.inner.downcast_mut::<T>()
3846 } else {
3847 None
3848 }
3849 } else {
3850 // If it's not a wrapper, try to downcast directly
3851 error.downcast_mut::<T>()
3852 }
3853 } else {
3854 None
3855 }
3856 }
3857
3858 /// Returns the primary context associated with this error.
3859 ///
3860 /// The primary context is typically the most recent or most relevant
3861 /// context added to the error, often containing the most specific
3862 /// information about the direct cause of the failure.
3863 ///
3864 /// # Returns
3865 ///
3866 /// An `Option` containing a reference to the primary `YoContext`,
3867 /// or `None` if no contexts have been added.
3868 ///
3869 /// # Examples
3870 ///
3871 /// ```
3872 /// use yoshi_std::{Yoshi, YoshiKind};
3873 ///
3874 /// let err = Yoshi::new(YoshiKind::Internal {
3875 /// message: "failed step".into(),
3876 /// source: None,
3877 /// component: None,
3878 /// })
3879 /// .context("Step 1 failed")
3880 /// .context("Step 2 failed"); // This is the primary context
3881 ///
3882 /// assert_eq!(err.primary_context().unwrap().message.as_deref(), Some("Step 2 failed"));
3883 /// ```
3884 #[inline]
3885 pub fn primary_context(&self) -> Option<&YoContext> {
3886 self.contexts.last()
3887 }
3888
3889 /// Returns an iterator over all contexts associated with this error.
3890 ///
3891 /// Contexts are ordered from oldest (first added) to newest (most recent, primary).
3892 ///
3893 /// # Returns
3894 ///
3895 /// An iterator yielding references to `YoContext` instances.
3896 ///
3897 /// # Examples
3898 ///
3899 /// ```
3900 /// use yoshi_std::{Yoshi, YoshiKind};
3901 ///
3902 /// let err = Yoshi::new(YoshiKind::Internal {
3903 /// message: "original error".into(),
3904 /// source: None,
3905 /// component: None,
3906 /// })
3907 /// .context("context 1")
3908 /// .context("context 2");
3909 ///
3910 /// let messages: Vec<_> = err.contexts().filter_map(|c| c.message.as_deref()).collect();
3911 /// assert_eq!(messages, vec!["context 1", "context 2"]);
3912 /// ```
3913 #[inline]
3914 pub fn contexts(&self) -> impl Iterator<Item = &YoContext> {
3915 self.contexts.iter()
3916 }
3917
3918 /// Returns the suggestion from the primary context, if any.
3919 ///
3920 /// This is a convenience method to quickly access the most relevant
3921 /// suggestion for resolving the error.
3922 ///
3923 /// # Returns
3924 ///
3925 /// An `Option` containing a reference to the suggestion string, or `None`.
3926 ///
3927 /// # Examples
3928 ///
3929 /// ```
3930 /// use yoshi_std::{Yoshi, YoshiKind};
3931 ///
3932 /// let err = Yoshi::new(YoshiKind::Io(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "file access denied")))
3933 /// .with_suggestion("Check file permissions.");
3934 ///
3935 /// assert_eq!(err.suggestion().as_deref(), Some("Check file permissions."));
3936 /// ```
3937 #[inline]
3938 pub fn suggestion(&self) -> Option<&str> {
3939 self.primary_context()
3940 .and_then(|ctx| ctx.suggestion.as_deref())
3941 }
3942
3943 /// Returns a typed shell from the primary context, if any.
3944 ///
3945 /// This is a convenience method to quickly access a structured shell
3946 /// from the most relevant context.
3947 ///
3948 /// # Type Parameters
3949 ///
3950 /// * `T` - The type of shell to retrieve.
3951 ///
3952 /// # Returns
3953 ///
3954 /// An `Option` containing a reference to the shell of type `T`, or `None`.
3955 ///
3956 /// # Examples
3957 ///
3958 /// ```
3959 /// use yoshi_std::{Yoshi, YoshiKind};
3960 /// #[derive(Debug, PartialEq)]
3961 /// struct CustomPayload(u32);
3962 /// let err = Yoshi::new(YoshiKind::Internal {
3963 /// message: "test".into(),
3964 /// source: None,
3965 /// component: None,
3966 /// })
3967 /// .with_shell(CustomPayload(123));
3968 /// /// assert_eq!(err.shell::<CustomPayload>().unwrap().0, 123);
3969 /// ```
3970 #[inline]
3971 pub fn shell<T: 'static>(&self) -> Option<&T> {
3972 // Search ALL contexts for the shell, not just the primary context
3973 // This ensures payloads can be found regardless of context priority ordering
3974 for context in &self.contexts {
3975 if let Some(shell) = context.shell::<T>() {
3976 return Some(shell);
3977 }
3978 }
3979 None
3980 }
3981
3982 /// The nested error, equivalent to `source()`, but more thematically expressive.
3983 ///
3984 /// This method provides thematic access to the underlying error source while
3985 /// maintaining full backwards compatibility with the standard `Error` trait.
3986 ///
3987 /// # Returns
3988 ///
3989 /// An `Option` containing a reference to the nested error, or `None` if
3990 /// there is no underlying source.
3991 ///
3992 /// # Examples
3993 ///
3994 /// ```rust
3995 /// use yoshi_std::{Yoshi, YoshiKind};
3996 /// # use std::io;
3997 /// let inner = Yoshi::new(YoshiKind::Internal {
3998 /// message: "inner failure".into(),
3999 /// source: None,
4000 /// component: None,
4001 /// });
4002 /// let outer = Yoshi::new(YoshiKind::Internal {
4003 /// message: "outer failure".into(),
4004 /// source: Some(Box::new(inner)),
4005 /// component: None,
4006 /// });
4007 /// assert!(outer.nest().is_some());
4008 /// ```
4009 #[inline]
4010 pub fn nest(&self) -> Option<&(dyn Error + 'static)> {
4011 self.kind.source()
4012 }
4013
4014 /// The explanation or context attached to the error.
4015 ///
4016 /// This method provides direct access to the primary context message,
4017 /// offering a thematic alternative to accessing context information.
4018 ///
4019 /// # Returns
4020 ///
4021 /// An `Option` containing a reference to the laytext string, or `None`
4022 /// if no context message is available.
4023 ///
4024 /// # Examples
4025 ///
4026 /// ```rust
4027 /// use yoshi_std::{Yoshi, YoshiKind};
4028 /// let err = Yoshi::new(YoshiKind::Internal {
4029 /// message: "base error".into(),
4030 /// source: None,
4031 /// component: None,
4032 /// })
4033 /// .context("operation failed");
4034 /// assert_eq!(err.laytext().unwrap(), "operation failed");
4035 /// ```
4036 #[inline]
4037 pub fn laytext(&self) -> Option<&str> {
4038 self.primary_context()
4039 .and_then(|ctx| ctx.message.as_deref())
4040 }
4041
4042 /// Adds contextual information using the thematic `.lay()` method.
4043 ///
4044 /// This method is equivalent to `.context()` but provides thematic naming
4045 /// consistent with the Hatch ecosystem's metaphorical framework.
4046 ///
4047 /// # Arguments
4048 ///
4049 /// * `msg` - The context message to attach.
4050 ///
4051 /// # Returns
4052 ///
4053 /// The modified `Yoshi` error instance with the new context.
4054 ///
4055 /// # Examples
4056 ///
4057 /// ```rust
4058 /// use yoshi_std::{Yoshi, YoshiKind};
4059 /// let err = Yoshi::new(YoshiKind::Internal {
4060 /// message: "base error".into(),
4061 /// source: None,
4062 /// component: None,
4063 /// })
4064 /// .lay("while processing request");
4065 /// assert!(err.to_string().contains("while processing request"));
4066 /// ```
4067 #[track_caller]
4068 #[inline]
4069 #[must_use]
4070 pub fn lay(self, msg: impl Into<String>) -> Self {
4071 self.context(msg)
4072 }
4073
4074 /// Gathers analysis results about the contexts in this error.
4075 ///
4076 /// This method performs a quick scan of all attached contexts to provide
4077 /// aggregated statistics, useful for logging, analytics, or deciding
4078 /// on error handling strategies.
4079 ///
4080 /// # Returns
4081 ///
4082 /// A `ContextAnalysis` struct containing various metrics about the contexts.
4083 ///
4084 /// # Examples
4085 ///
4086 /// ```
4087 /// use yoshi_std::{Yoshi, YoshiKind, YoshiLocation};
4088 ///
4089 /// let err = Yoshi::new(YoshiKind::Internal {
4090 /// message: "base error".into(),
4091 /// source: None,
4092 /// component: None,
4093 /// })
4094 /// .context("Intermediate step")
4095 /// .with_metadata("key", "value")
4096 /// .with_suggestion("Try again")
4097 /// .context("Final step failed")
4098 /// .with_location(YoshiLocation::new("src/main.rs", 10, 5));
4099 ///
4100 /// let analysis = err.analyze_contexts();
4101 /// assert_eq!(analysis.total_contexts, 2);
4102 /// assert_eq!(analysis.context_depth, 2);
4103 /// assert!(analysis.has_suggestions);
4104 /// assert!(analysis.has_location_info);
4105 /// assert_eq!(analysis.metadata_entries, 1);
4106 /// ```
4107 pub fn analyze_contexts(&self) -> ContextAnalysis {
4108 let mut analysis = ContextAnalysis {
4109 total_contexts: self.contexts.len(),
4110 context_depth: self.contexts.len(), // Simple depth = count for now
4111 ..ContextAnalysis::default()
4112 };
4113
4114 for ctx in &self.contexts {
4115 if ctx.suggestion.is_some() {
4116 analysis.has_suggestions = true;
4117 }
4118 if ctx.location.is_some() {
4119 analysis.has_location_info = true;
4120 }
4121 analysis.metadata_entries += ctx.metadata.len();
4122 analysis.typed_payloads += ctx.payloads.len();
4123
4124 // The primary context is the last one in the vector
4125 if let Some(primary_ctx) = self.contexts.last() {
4126 analysis.primary_context_priority = primary_ctx.priority;
4127 }
4128 }
4129 analysis
4130 }
4131}
4132
4133impl Display for Yoshi {
4134 /// Formats the `Yoshi` error for display with optimized O(n) error chain traversal.
4135 ///
4136 /// This implementation provides a comprehensive, human-readable representation
4137 /// of the error, designed for debugging and logging. It uses an optimized
4138 /// iterative approach to traverse error chains, eliminating the O(n²) performance
4139 /// bottleneck present in recursive formatting. The formatter collects the entire
4140 /// error chain first, then renders all information in a single linear pass.
4141 ///
4142 /// # Performance Characteristics
4143 ///
4144 /// - **Time Complexity**: O(n) where n is the total depth of the error chain
4145 /// - **Space Complexity**: O(n) for temporary chain storage
4146 /// - **Memory Allocation**: Minimized through `OptimizedFormatBuffer` usage
4147 /// - **Scaling**: Linear performance even for deep error chains (100+ levels)
4148 ///
4149 /// # Arguments
4150 ///
4151 /// * `f` - The formatter to write into.
4152 ///
4153 /// # Returns
4154 ///
4155 /// A `fmt::Result` indicating success or failure of the formatting.
4156 ///
4157 /// # Examples
4158 ///
4159 /// ```
4160 /// # use yoshi_std::{Yoshi, YoshiKind};
4161 /// let error = Yoshi::new(YoshiKind::Internal {
4162 /// message: "Operation failed".into(),
4163 /// source: None,
4164 /// component: None,
4165 /// })
4166 /// .context("While processing request");
4167 ///
4168 /// println!("{}", error); // Efficient O(n) formatting
4169 /// ```
4170 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
4171 // Use optimized buffer for efficient string building
4172 let mut buffer = OptimizedFormatBuffer::new();
4173
4174 // Write primary error information
4175 buffer.append_optimized(&format!("{}: {}", self.instance_id, self.kind));
4176 buffer.append_optimized("\n");
4177
4178 // Print contexts from oldest to newest (excluding auto-generated ones)
4179 for (i, ctx) in self.contexts.iter().enumerate() {
4180 if i == 0
4181 && ctx.message.as_deref() == Some("Error occurred")
4182 && ctx.metadata.is_empty()
4183 && ctx.suggestion.is_none()
4184 && ctx.payloads.is_empty()
4185 {
4186 // Skip auto-generated default context if it provides no actual info
4187 continue;
4188 }
4189
4190 if let Some(msg) = ctx.message.as_deref() {
4191 buffer.append_optimized("Caused by: ");
4192 buffer.append_optimized(msg);
4193 buffer.append_optimized("\n");
4194 }
4195
4196 if !ctx.metadata.is_empty() {
4197 buffer.append_optimized("Metadata:\n");
4198 for (k, v) in &ctx.metadata {
4199 buffer.append_optimized(" ");
4200 buffer.append_optimized(k.as_ref());
4201 buffer.append_optimized(": ");
4202 buffer.append_optimized(v.as_ref());
4203 buffer.append_optimized("\n");
4204 }
4205 }
4206
4207 if let Some(suggestion) = ctx.suggestion.as_deref() {
4208 buffer.append_optimized("Suggestion: ");
4209 buffer.append_optimized(suggestion);
4210 buffer.append_optimized("\n");
4211 }
4212
4213 if let Some(location) = ctx.location {
4214 buffer.append_optimized("Location: ");
4215 buffer.append_optimized(&location.to_string());
4216 buffer.append_optimized("\n");
4217 }
4218 }
4219
4220 // Collect complete error chain iteratively (O(n) instead of O(n²))
4221 let mut error_chain = Vec::new();
4222 let mut yoshi_contexts = Vec::new();
4223
4224 // Start with the source from this error's kind
4225 let mut current_error = self.kind.source();
4226
4227 while let Some(source_error) = current_error {
4228 // Check if it's a Yoshi error to extract contexts
4229 if let Some(yoshi_source) = source_error.downcast_ref::<Yoshi>() {
4230 // Add the Yoshi error's kind to the chain
4231 error_chain.push(format!("Caused by: {}", yoshi_source.kind));
4232
4233 // Collect contexts from this Yoshi error
4234 for ctx in &yoshi_source.contexts {
4235 if let Some(msg) = ctx.message.as_deref() {
4236 yoshi_contexts.push(format!("Caused by: {msg}"));
4237 }
4238 }
4239
4240 // Move to the next error in the chain
4241 current_error = yoshi_source.kind.source();
4242 } else {
4243 // For non-Yoshi sources, add directly to chain and stop
4244 error_chain.push(format!("Caused by: {source_error}"));
4245 current_error = source_error.source();
4246 }
4247 }
4248
4249 // Append all collected error chain information
4250 for error_msg in error_chain {
4251 buffer.append_optimized(&error_msg);
4252 buffer.append_optimized("\n");
4253 }
4254
4255 // Append all collected Yoshi contexts
4256 for ctx_msg in yoshi_contexts {
4257 buffer.append_optimized(&ctx_msg);
4258 buffer.append_optimized("\n");
4259 }
4260
4261 // Add backtrace if available
4262 #[cfg(feature = "std")]
4263 if let Some(bt) = &self.backtrace {
4264 buffer.append_optimized("\nBacktrace:\n");
4265 buffer.append_optimized(&bt.to_string());
4266 }
4267
4268 // Write the complete formatted output
4269 write!(f, "{}", buffer.as_str().trim_end())
4270 }
4271}
4272
4273#[cfg(feature = "std")]
4274impl Error for Yoshi {
4275 /// Returns the underlying source of this error.
4276 ///
4277 /// This method provides access to the root cause of the error chain,
4278 /// enabling compatibility with Rust's standard error handling mechanisms.
4279 ///
4280 /// # Returns
4281 ///
4282 /// An `Option` containing a reference to the `dyn Error` source,
4283 /// or `None` if there is no underlying cause.
4284 fn source(&self) -> Option<&(dyn Error + 'static)> {
4285 self.kind.source()
4286 }
4287}
4288
4289#[cfg(not(feature = "std"))]
4290impl Error for Yoshi {
4291 /// Returns the underlying source of this error.
4292 ///
4293 /// This method provides access to the root cause of the error chain,
4294 /// enabling compatibility with Rust's standard error handling mechanisms.
4295 ///
4296 /// # Returns
4297 ///
4298 /// An `Option` containing a reference to the `dyn Error` source,
4299 /// or `None` if there is no underlying cause.
4300 fn source(&self) -> Option<&(dyn Error + 'static)> {
4301 self.kind.source()
4302 }
4303}
4304
4305impl From<String> for Yoshi {
4306 /// Converts a `String` into a `Yoshi` error.
4307 ///
4308 /// The string message is wrapped in an `Internal` `YoshiKind`.
4309 ///
4310 /// # Arguments
4311 ///
4312 /// * `s` - The string message to convert.
4313 ///
4314 /// # Returns
4315 ///
4316 /// A new `Yoshi` error instance.
4317 #[track_caller]
4318 fn from(s: String) -> Self {
4319 Yoshi::new(YoshiKind::Internal {
4320 message: s.into(),
4321 source: None,
4322 component: None,
4323 })
4324 }
4325}
4326
4327impl From<&str> for Yoshi {
4328 /// Converts a string slice (`&str`) into a `Yoshi` error.
4329 ///
4330 /// The string slice is converted to a `String` and then wrapped in an
4331 /// `Internal` `YoshiKind`.
4332 ///
4333 /// # Arguments
4334 ///
4335 /// * `s` - The string slice to convert.
4336 ///
4337 /// # Returns
4338 ///
4339 /// A new `Yoshi` error instance.
4340 #[track_caller]
4341 fn from(s: &str) -> Self {
4342 Yoshi::new(YoshiKind::Internal {
4343 message: s.to_string().into(),
4344 source: None,
4345 component: None,
4346 })
4347 }
4348}
4349
4350#[cfg(feature = "std")]
4351impl From<std::io::Error> for Yoshi {
4352 /// Converts a `std::io::Error` into a `Yoshi` error.
4353 ///
4354 /// The I/O error is wrapped in a `YoshiKind::Io` variant.
4355 ///
4356 /// # Arguments
4357 ///
4358 /// * `e` - The `std::io::Error` to convert.
4359 ///
4360 /// # Returns
4361 ///
4362 /// A new `Yoshi` error instance.
4363 #[track_caller]
4364 fn from(e: std::io::Error) -> Self {
4365 Yoshi::new(YoshiKind::Io(e))
4366 }
4367}
4368
4369#[cfg(not(feature = "std"))]
4370impl From<NoStdIo> for Yoshi {
4371 /// Converts a `NoStdIo` error into a `Yoshi` error.
4372 ///
4373 /// The `NoStdIo` error is wrapped in a `YoshiKind::Io` variant.
4374 ///
4375 /// # Arguments
4376 ///
4377 /// * `e` - The `NoStdIo` error to convert.
4378 ///
4379 /// # Returns
4380 ///
4381 /// A new `Yoshi` error instance.
4382 #[track_caller]
4383 fn from(e: NoStdIo) -> Self {
4384 Yoshi::new(YoshiKind::Io(e))
4385 }
4386}
4387
4388impl<T, E> HatchExt<T> for core::result::Result<T, E>
4389where
4390 E: Into<Yoshi> + Send + Sync + 'static, // Updated trait bounds
4391{
4392 #[track_caller]
4393 #[inline]
4394 fn context(self, msg: impl Into<String>) -> Result<T> {
4395 self.map_err(|e| e.into().context(msg))
4396 }
4397
4398 #[track_caller]
4399 #[inline]
4400 fn with_suggestion(self, s: impl Into<String>) -> Result<T> {
4401 self.map_err(|e| e.into().with_suggestion(s))
4402 }
4403 #[track_caller]
4404 #[inline]
4405 fn with_shell(self, p: impl Any + Send + Sync + 'static) -> Result<T> {
4406 self.map_err(|e| {
4407 let mut yoshi_err = e.into();
4408 // Ensure we have a context to attach the shell to with standard priority
4409 if yoshi_err.contexts.is_empty() {
4410 yoshi_err
4411 .contexts
4412 .push(YoContext::default().with_priority(128));
4413 }
4414 yoshi_err.with_shell(p)
4415 })
4416 }
4417
4418 /// Sets the priority for the error's primary context.
4419 #[track_caller]
4420 #[inline]
4421 fn with_priority(self, priority: u8) -> Result<T> {
4422 self.map_err(|e| e.into().with_priority(priority))
4423 }
4424
4425 // NEW: Short aliases - just delegate to the full methods
4426 #[track_caller]
4427 #[inline]
4428 fn ctx(self, msg: impl Into<String>) -> Result<T> {
4429 self.context(msg)
4430 }
4431
4432 #[track_caller]
4433 #[inline]
4434 fn help(self, s: impl Into<String>) -> Result<T> {
4435 self.with_suggestion(s)
4436 }
4437
4438 #[track_caller]
4439 #[inline]
4440 fn meta(self, k: impl Into<String>, v: impl Into<String>) -> Result<T> {
4441 self.map_err(|e| {
4442 let mut yoshi_err = e.into();
4443 // Ensure we have a context to attach metadata to with proper priority
4444 if yoshi_err.contexts.is_empty() {
4445 yoshi_err
4446 .contexts
4447 .push(YoContext::default().with_priority(128));
4448 }
4449 yoshi_err.with_metadata(k, v)
4450 })
4451 }
4452}
4453
4454/// Trait that adds `.lay(...)` to `Result<T, Yoshi>`, enriching errors with context.
4455///
4456/// This trait provides ergonomic context attachment using thematic naming that
4457/// aligns with the Yoshi metaphorical framework. The `.lay()` method is equivalent
4458/// to adding context but uses intuitive, game-inspired terminology.
4459///
4460/// # Performance Characteristics
4461///
4462/// - **Context Addition**: O(1) operation with minimal memory allocation
4463/// - **String Interning**: Automatic optimization for repeated context messages
4464/// - **Memory Efficiency**: Shared storage for common context patterns
4465///
4466/// # Examples
4467///
4468/// ```rust
4469/// use yoshi_std::{Hatch, LayContext, Yoshi, YoshiKind};
4470///
4471/// fn database_operation() -> Hatch<String> {
4472/// Err(Yoshi::new(YoshiKind::Internal {
4473/// message: "connection failed".into(),
4474/// source: None,
4475/// component: None,
4476/// }))
4477/// .lay("While establishing database connection")
4478/// }
4479/// ```
4480pub trait LayContext<T> {
4481 /// Adds a contextual message to the error chain, like laying an egg with metadata.
4482 ///
4483 /// This method enriches error information by attaching descriptive context
4484 /// that helps with debugging and error tracing. It uses thematic naming
4485 /// inspired by Yoshi's egg-laying ability to create memorable, intuitive APIs.
4486 ///
4487 /// # Arguments
4488 ///
4489 /// * `message` - The context message to attach. Accepts any type that converts to `String`.
4490 ///
4491 /// # Returns
4492 ///
4493 /// A `Hatch<T>` with the enriched context information attached.
4494 ///
4495 /// # Performance
4496 ///
4497 /// - **Time Complexity**: O(1) for context attachment
4498 /// - **Memory Optimization**: Automatic string interning for efficiency
4499 /// - **Allocation Pattern**: Minimal heap allocation with shared storage
4500 ///
4501 /// # Examples
4502 ///
4503 /// ```rust
4504 /// use yoshi_std::{Hatch, LayContext, Yoshi, YoshiKind};
4505 ///
4506 /// let result: Hatch<()> = Err(Yoshi::new(YoshiKind::Internal {
4507 /// message: "operation failed".into(),
4508 /// source: None,
4509 /// component: None,
4510 /// }))
4511 /// .lay("During user authentication");
4512 ///
4513 /// assert!(result.is_err());
4514 /// let error = result.unwrap_err();
4515 /// assert!(error.to_string().contains("During user authentication"));
4516 /// ```
4517 ///
4518 /// # Errors
4519 ///
4520 /// Returns the enriched `Hatch<T>` error if `self` is `Err`, or the original
4521 /// success value if `self` is `Ok`. This method never introduces new errors.
4522 fn lay(self, message: impl Into<String>) -> Hatch<T>;
4523}
4524
4525impl<T> LayContext<T> for Hatch<T> {
4526 #[track_caller]
4527 fn lay(self, message: impl Into<String>) -> Hatch<T> {
4528 self.map_err(|e| e.lay(message))
4529 }
4530}
4531
4532/// Extension trait for mapping other `Result<T, E>` types into `Hatch<T>` easily.
4533///
4534/// This trait enables seamless integration between the Yoshi error ecosystem and
4535/// external error types. It provides the `.hatch()` method that converts any
4536/// `Result` with an error type that can be converted to `Yoshi` into a `Hatch<T>`.
4537///
4538/// # Type Requirements
4539///
4540/// The error type `E` must implement `Into<Yoshi>` to enable conversion. This is
4541/// automatically satisfied for:
4542/// - `std::io::Error` (when std feature is enabled)
4543/// - `NoStdIo` (when std feature is disabled)
4544/// - `String` and `&str` types
4545/// - Any type that implements `std::error::Error + Send + Sync + 'static`
4546///
4547/// # Performance Characteristics
4548///
4549/// - **Conversion Cost**: O(1) for types with direct `Into<Yoshi>` implementations
4550/// - **Memory Overhead**: Minimal - reuses existing error allocation where possible
4551/// - **Type Safety**: Compile-time guarantees with no runtime type checking
4552///
4553/// # Examples
4554///
4555/// ```rust
4556/// use yoshi_std::{Hatch, Hatchable, LayContext};
4557/// # use std::io;
4558///
4559/// fn file_operation() -> Hatch<String> {
4560/// std::fs::read_to_string("config.toml")
4561/// .hatch() // Convert io::Error to Yoshi
4562/// .lay("While reading configuration file")
4563/// }
4564///
4565/// fn parse_operation() -> Hatch<i32> {
4566/// "not_a_number".parse::<i32>()
4567/// .map_err(|e| e.to_string()) // Convert to String first
4568/// .hatch() // Then convert to Yoshi
4569/// .lay("While parsing user input")
4570/// }
4571/// ```
4572pub trait Hatchable<T, E> {
4573 /// Converts an error into a `Hatch<T>` by mapping it into `Yoshi`.
4574 ///
4575 /// This method provides a convenient way to bring external error types into
4576 /// the Yoshi ecosystem while maintaining type safety and performance efficiency.
4577 /// The conversion leverages existing `Into<Yoshi>` implementations to minimize
4578 /// overhead and maintain semantic meaning.
4579 ///
4580 /// # Type Conversion Chain
4581 ///
4582 /// The method works by applying the following transformation:
4583 /// `Result<T, E>` → `Result<T, Yoshi>` (via `E: Into<Yoshi>`)
4584 ///
4585 /// # Returns
4586 ///
4587 /// A `Hatch<T>` containing either the original success value or the converted error.
4588 ///
4589 /// # Performance Considerations
4590 ///
4591 /// - **Zero-cost for compatible types**: When `E` already has efficient `Into<Yoshi>`
4592 /// - **Minimal allocation**: Reuses existing error data structures where possible
4593 /// - **Compile-time optimization**: Fully optimizable conversion chains
4594 ///
4595 /// # Examples
4596 ///
4597 /// ```rust
4598 /// use yoshi_std::{Hatch, Hatchable};
4599 /// # use std::io;
4600 ///
4601 /// // I/O error conversion
4602 /// let io_result: Result<String, io::Error> = Err(io::Error::new(
4603 /// io::ErrorKind::NotFound, "file not found"
4604 /// ));
4605 /// let hatched: Hatch<String> = io_result.hatch();
4606 /// assert!(hatched.is_err());
4607 ///
4608 /// // String error conversion
4609 /// let string_result: Result<i32, String> = Err("parsing failed".to_string());
4610 /// let hatched: Hatch<i32> = string_result.hatch();
4611 /// assert!(hatched.is_err());
4612 /// ```
4613 ///
4614 /// # Errors
4615 ///
4616 /// Returns a `Hatch<T>` containing the converted error if `self` is `Err`,
4617 /// or the original success value if `self` is `Ok`. Conversion errors are
4618 /// not possible as the `Into<Yoshi>` bound guarantees valid transformation.
4619 fn hatch(self) -> Hatch<T>;
4620}
4621
4622impl<T, E: Into<Yoshi>> Hatchable<T, E> for Result<T, E> {
4623 #[track_caller]
4624 fn hatch(self) -> Hatch<T> {
4625 self.map_err(Into::into)
4626 }
4627}
4628
4629//--------------------------------------------------------------------------------------------------
4630// Enhanced backtrace capture with performance monitoring
4631//--------------------------------------------------------------------------------------------------
4632
4633/// Conditionally captures a `YoshiBacktrace` based on environment variables.
4634///
4635/// This private helper function checks the `RUST_LIB_BACKTRACE` and `RUST_BACKTRACE`
4636/// environment variables. If either is set to "1" or "full", a [`YoshiBacktrace`]
4637/// is captured and returned. Otherwise, it returns `None`.
4638/// This ensures backtraces are only generated when explicitly requested,
4639/// minimizing performance overhead in production.
4640///
4641/// # Returns
4642///
4643/// An `Option` containing a [`YoshiBacktrace`] if backtrace capture is enabled,
4644/// or `None` otherwise.
4645///
4646/// # Panics
4647///
4648/// This function will panic if `OnceLock::get_or_init` is called in a `no_std` context
4649/// as its placeholder implementation panics. However, this function itself is
4650/// `#[cfg(feature = "std")]`, so it won't be compiled in `no_std`.
4651#[cfg(feature = "std")]
4652fn capture_bt() -> Option<YoshiBacktrace> {
4653 // For more robust behavior, especially in testing environments,
4654 // check the environment variables directly each time instead of caching
4655 let should =
4656 match std::env::var("RUST_LIB_BACKTRACE").or_else(|_| std::env::var("RUST_BACKTRACE")) {
4657 Ok(v) => v == "1" || v == "full", // Only enable backtrace for specific values
4658 Err(_) => false,
4659 };
4660
4661 if should {
4662 Some(YoshiBacktrace::new_captured())
4663 } else {
4664 None
4665 }
4666}
4667
4668/// Enhanced memory management utilities
4669pub mod memory {
4670 use super::{error_instance_count, intern_string, Arc, String, STRING_INTERN_POOL};
4671 /// Memory usage statistics for error handling
4672 #[derive(Debug, Default)]
4673 pub struct MemoryStats {
4674 /// Total number of Yoshi error instances created since application start
4675 pub total_errors_created: u32,
4676 /// Total number of context objects created across all errors
4677 pub total_contexts_created: u64,
4678 /// Number of string interning cache hits for memory optimization
4679 pub string_intern_hits: usize,
4680 /// Number of string interning cache misses requiring new allocations
4681 pub string_intern_misses: usize,
4682 /// Estimated bytes saved through string interning and optimization
4683 pub estimated_memory_saved: usize,
4684 }
4685
4686 /// Get comprehensive memory usage statistics
4687 pub fn get_memory_stats() -> MemoryStats {
4688 let (hits, misses) = STRING_INTERN_POOL
4689 .get()
4690 .map_or((0, 0), super::StringInternPool::stats);
4691
4692 MemoryStats {
4693 total_errors_created: error_instance_count(),
4694 total_contexts_created: 0, // Would need tracking
4695 string_intern_hits: hits,
4696 string_intern_misses: misses,
4697 estimated_memory_saved: hits * 32, // Rough estimate
4698 }
4699 }
4700
4701 /// Memory-efficient string creation with automatic interning
4702 pub fn efficient_string(s: impl Into<String>) -> Arc<str> {
4703 intern_string(s)
4704 }
4705
4706 /// Triggers cleanup of the string interning pool for long-running applications
4707 #[cfg(feature = "std")]
4708 pub fn cleanup_intern_pool() {
4709 if let Some(pool) = STRING_INTERN_POOL.get() {
4710 pool.clear_pool();
4711 }
4712 }
4713}
4714
4715//--------------------------------------------------------------------------------------------------
4716// Advanced async error handling module with Rust 1.87 enhancements
4717//--------------------------------------------------------------------------------------------------
4718
4719#[cfg(feature = "std")]
4720pub mod async_error_handling {
4721 //! Advanced async error processing utilities with precise capturing and performance optimization.
4722
4723 use super::{Result, String, Vec, Yoshi, YoshiKind};
4724 use std::future::Future;
4725 use std::time::Duration;
4726
4727 #[cfg(feature = "async")]
4728 #[allow(unused_imports)]
4729 use tokio::time;
4730
4731 /// Async error propagation with enhanced context preservation
4732 ///
4733 /// # Errors
4734 ///
4735 /// Returns a `Yoshi` error if the future resolves to an error, with additional context added.
4736 pub async fn propagate_async<T, E>(
4737 future: impl Future<Output = Result<T, E>>,
4738 context: impl Into<String>,
4739 ) -> Result<T, Yoshi>
4740 where
4741 E: Into<Yoshi>,
4742 {
4743 match future.await {
4744 Ok(value) => Ok(value),
4745 Err(error) => Err(error.into().context(context.into())),
4746 }
4747 }
4748
4749 /// Async error recovery with exponential backoff
4750 ///
4751 /// # Errors
4752 ///
4753 /// Returns a `Yoshi` error if all retry attempts fail or if the error is not transient.
4754 pub async fn retry_with_backoff<T, F, Fut>(
4755 mut operation: F,
4756 max_retries: usize,
4757 base_delay: Duration,
4758 ) -> Result<T, Yoshi>
4759 where
4760 F: FnMut() -> Fut,
4761 Fut: Future<Output = Result<T, Yoshi>>,
4762 {
4763 let mut delay = base_delay;
4764
4765 for attempt in 0..=max_retries {
4766 match operation().await {
4767 Ok(result) => return Ok(result),
4768 Err(error) if attempt == max_retries => return Err(error),
4769 Err(error) if error.is_transient() => {
4770 // Use async sleep for proper async compatibility
4771 #[cfg(feature = "async")]
4772 tokio::time::sleep(delay).await;
4773 #[cfg(not(feature = "async"))]
4774 std::thread::sleep(delay);
4775 delay *= 2;
4776 }
4777 Err(error) => return Err(error),
4778 }
4779 }
4780
4781 unreachable!()
4782 }
4783
4784 /// Async error aggregation for parallel operations
4785 ///
4786 /// # Errors
4787 ///
4788 /// Returns a `Yoshi` error with multiple errors aggregated if any operations fail.
4789 pub async fn aggregate_errors<I, F, Fut, T>(operations: I) -> Result<Vec<T>, Yoshi>
4790 where
4791 I: IntoIterator<Item = F>,
4792 F: FnOnce() -> Fut,
4793 Fut: Future<Output = Result<T, Yoshi>>,
4794 {
4795 let futures: Vec<_> = operations.into_iter().map(|op| op()).collect();
4796 // Simple join_all implementation without futures dependency
4797 let mut results = Vec::new();
4798 for fut in futures {
4799 results.push(fut.await);
4800 }
4801
4802 let mut successes = Vec::new();
4803 let mut errors = Vec::new();
4804
4805 for result in results {
4806 match result {
4807 Ok(value) => successes.push(value),
4808 Err(error) => errors.push(error),
4809 }
4810 }
4811
4812 if errors.is_empty() {
4813 Ok(successes)
4814 } else {
4815 Err(Yoshi::new(YoshiKind::Multiple {
4816 errors,
4817 primary_index: Some(0),
4818 }))
4819 }
4820 }
4821}
4822
4823//--------------------------------------------------------------------------------------------------
4824// Cross-process communication and error reporting
4825//--------------------------------------------------------------------------------------------------
4826
4827#[cfg(all(feature = "std", feature = "serde"))]
4828pub mod process_communication {
4829 //! Cross-process error reporting and coordination with enterprise-grade reliability.
4830
4831 use super::{Arc, HashMap, OnceLock, Result, String, SystemTime, ToString, Yoshi};
4832 use serde::{self, Deserializer, Serializer};
4833 use serde_json;
4834 use std::sync::mpsc;
4835 use std::thread;
4836
4837 // Helper functions for SystemTime serialization/deserialization for std
4838 // (Serializes as seconds since UNIX_EPOCH)
4839 mod serde_system_time {
4840 use super::{Deserializer, Serializer};
4841 use serde::Deserialize;
4842 use std::time::{SystemTime, UNIX_EPOCH};
4843 #[allow(clippy::trivially_copy_pass_by_ref)]
4844 pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
4845 where
4846 S: Serializer,
4847 {
4848 let duration = time
4849 .duration_since(UNIX_EPOCH)
4850 .map_err(serde::ser::Error::custom)?;
4851 serializer.serialize_u64(duration.as_secs())
4852 }
4853
4854 pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
4855 where
4856 D: Deserializer<'de>,
4857 {
4858 let secs = u64::deserialize(deserializer)?;
4859 Ok(UNIX_EPOCH + std::time::Duration::from_secs(secs))
4860 }
4861 }
4862
4863 /// Cross-process error reporter with structured logging
4864 pub struct ProcessErrorReporter {
4865 sender: mpsc::Sender<ProcessError>,
4866 _handle: thread::JoinHandle<()>,
4867 }
4868 /// Serializable error for cross-process communication
4869 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
4870 pub struct ProcessError {
4871 /// Unique identifier for the process that generated this error
4872 pub process_id: u32,
4873 /// String identifier for the thread within the process
4874 pub thread_id: String,
4875 /// Human-readable error message describing the failure
4876 pub error_message: String,
4877 /// Classification of the error type for categorization
4878 pub error_kind: String,
4879 /// Severity level from 0 (info) to 255 (critical)
4880 pub severity: u8,
4881 /// System timestamp when the error occurred
4882 #[serde(with = "serde_system_time")]
4883 pub timestamp: SystemTime,
4884 /// Additional metadata for enhanced error context
4885 #[serde(
4886 serialize_with = "super::serde_helpers::serialize_arc_str_map",
4887 deserialize_with = "super::serde_helpers::deserialize_arc_str_map"
4888 )]
4889 pub metadata: HashMap<Arc<str>, Arc<str>>,
4890 }
4891
4892 impl Default for ProcessErrorReporter {
4893 fn default() -> Self {
4894 Self::new()
4895 }
4896 }
4897
4898 impl ProcessErrorReporter {
4899 /// Creates a new process error reporter with background processing
4900 #[must_use]
4901 pub fn new() -> Self {
4902 let (sender, receiver) = mpsc::channel::<ProcessError>();
4903
4904 let handle = thread::spawn(move || {
4905 while let Ok(error) = receiver.recv() {
4906 // Process and log the error
4907 eprintln!(
4908 "[PROCESS-ERROR] {}: {} (PID: {}, Severity: {})",
4909 error.timestamp.elapsed().map(|d| d.as_secs()).unwrap_or(0),
4910 error.error_message,
4911 error.process_id,
4912 error.severity
4913 ); // Write to structured log file (using serde_json for robust serialization)
4914 if let Ok(json_log) = serde_json::to_string(&error) {
4915 println!("STRUCTURED_LOG: {json_log}");
4916 } else {
4917 eprintln!("Failed to serialize process error to JSON.");
4918 }
4919 }
4920 });
4921
4922 Self {
4923 sender,
4924 _handle: handle,
4925 }
4926 }
4927 /// Reports an error to the cross-process system
4928 ///
4929 /// # Errors
4930 ///
4931 /// Returns `mpsc::SendError<ProcessError>` if the cross-process communication
4932 /// channel is disconnected or the receiver has been dropped.
4933 pub fn report_error(&self, error: &Yoshi) -> Result<(), mpsc::SendError<ProcessError>> {
4934 let process_error = ProcessError {
4935 process_id: std::process::id(),
4936 thread_id: format!("{:?}", std::thread::current().id()),
4937 error_message: error.to_string(),
4938 error_kind: format!("{:?}", error.kind()),
4939 severity: error.severity(),
4940 timestamp: SystemTime::now(),
4941 metadata: error
4942 .primary_context()
4943 .map(|ctx| ctx.metadata.clone())
4944 .unwrap_or_default(),
4945 };
4946
4947 self.sender.send(process_error)
4948 }
4949 }
4950
4951 /// Global process error coordinator
4952 static PROCESS_REPORTER: OnceLock<ProcessErrorReporter> = OnceLock::new();
4953
4954 /// Gets or initializes the global process error reporter
4955 pub fn global_reporter() -> &'static ProcessErrorReporter {
4956 PROCESS_REPORTER.get_or_init(ProcessErrorReporter::new)
4957 }
4958
4959 /// Reports an error to the global cross-process system
4960 pub fn report_global_error(error: &Yoshi) {
4961 if let Err(e) = global_reporter().report_error(error) {
4962 eprintln!("Failed to report error to cross-process system: {e}");
4963 }
4964 }
4965}
4966
4967//--------------------------------------------------------------------------------------------------
4968// SIMD-optimized string processing for high-performance formatting
4969//--------------------------------------------------------------------------------------------------
4970
4971#[cfg(all(feature = "unstable-metrics", target_arch = "x86_64"))]
4972pub mod simd_optimization {
4973 //! SIMD-accelerated string processing for optimal error formatting performance.
4974
4975 use super::{String, ToString, Vec, Yoshi};
4976
4977 /// SIMD-optimized string formatting buffer
4978 pub struct SimdFormatBuffer {
4979 data: Vec<u8>,
4980 capacity: usize,
4981 }
4982
4983 impl SimdFormatBuffer {
4984 /// Creates a new SIMD-optimized format buffer
4985 #[must_use]
4986 pub fn new() -> Self {
4987 Self::with_capacity(4096)
4988 }
4989
4990 /// Creates a buffer with specified capacity aligned for SIMD operations
4991 #[must_use]
4992 pub fn with_capacity(capacity: usize) -> Self {
4993 // Align capacity to 32-byte boundaries for AVX2 operations
4994 let aligned_capacity = (capacity + 31) & !31;
4995 Self {
4996 data: Vec::with_capacity(aligned_capacity),
4997 capacity: aligned_capacity,
4998 }
4999 }
5000
5001 /// SIMD-accelerated string concatenation
5002 pub fn append_simd(&mut self, s: &str) {
5003 let bytes = s.as_bytes();
5004 let new_len = self.data.len() + bytes.len();
5005
5006 if new_len > self.capacity {
5007 self.grow_aligned(new_len);
5008 }
5009
5010 // Use SIMD operations for large strings
5011 if bytes.len() >= 32 {
5012 unsafe { self.append_simd_internal(bytes) };
5013 } else {
5014 self.data.extend_from_slice(bytes);
5015 }
5016 }
5017
5018 /// Internal SIMD implementation using safe intrinsics
5019 #[target_feature(enable = "avx2")]
5020 unsafe fn append_simd_internal(&mut self, bytes: &[u8]) {
5021 #[cfg(target_arch = "x86_64")]
5022 {
5023 use std::arch::x86_64::{_mm256_loadu_si256, _mm256_storeu_si256};
5024
5025 let chunks = bytes.chunks_exact(32);
5026 let remainder = chunks.remainder();
5027 for chunk in chunks {
5028 let simd_data = _mm256_loadu_si256(chunk.as_ptr().cast());
5029 let dst_ptr = self.data.as_mut_ptr().add(self.data.len()).cast();
5030 _mm256_storeu_si256(dst_ptr, simd_data);
5031 self.data.set_len(self.data.len() + 32);
5032 }
5033
5034 // Handle remaining bytes
5035 if !remainder.is_empty() {
5036 self.data.extend_from_slice(remainder);
5037 }
5038 }
5039 }
5040
5041 /// Grows the buffer with proper alignment
5042 fn grow_aligned(&mut self, min_capacity: usize) {
5043 let new_capacity = ((min_capacity * 2) + 31) & !31;
5044 self.data.reserve_exact(new_capacity - self.data.capacity());
5045 self.capacity = new_capacity;
5046 }
5047 /// Returns the formatted string
5048 #[must_use]
5049 pub fn as_str(&self) -> &str {
5050 // SAFETY: We only append valid UTF-8 strings
5051 unsafe { std::str::from_utf8_unchecked(&self.data) }
5052 }
5053
5054 /// Clears the buffer while preserving capacity
5055 pub fn clear(&mut self) {
5056 self.data.clear();
5057 }
5058 }
5059
5060 impl Default for SimdFormatBuffer {
5061 fn default() -> Self {
5062 Self::new()
5063 }
5064 }
5065
5066 /// SIMD-optimized error formatting
5067 pub fn format_error_simd(error: &Yoshi) -> String {
5068 let mut buffer = SimdFormatBuffer::new();
5069
5070 // Format main error
5071 buffer.append_simd(&format!("{}", error.kind()));
5072
5073 // Add contexts with SIMD acceleration
5074 for context in error.contexts() {
5075 if let Some(ref message) = context.message {
5076 buffer.append_simd("\nCaused by: ");
5077 buffer.append_simd(message);
5078 }
5079 }
5080
5081 buffer.as_str().to_string()
5082 }
5083}
5084
5085//--------------------------------------------------------------------------------------------------
5086// Cross-process metrics and telemetry
5087//--------------------------------------------------------------------------------------------------
5088
5089#[cfg(feature = "unstable-metrics")]
5090pub mod cross_process_metrics {
5091 //! Global error metrics and telemetry system with cross-process coordination.
5092
5093 use super::{OnceLock, SystemTime, Yoshi};
5094 use std::collections::HashMap;
5095 use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
5096
5097 /// Global error metrics collector
5098 #[derive(Debug)]
5099 pub struct ErrorMetrics {
5100 total_errors: AtomicU32,
5101 #[allow(dead_code)]
5102 errors_by_kind: HashMap<&'static str, AtomicU32>,
5103 errors_by_severity: [AtomicU32; 256],
5104 memory_usage: AtomicUsize,
5105 #[allow(dead_code)]
5106 processing_time: AtomicU32,
5107 }
5108 impl Default for ErrorMetrics {
5109 /// Creates a new metrics collector
5110 fn default() -> Self {
5111 Self {
5112 total_errors: AtomicU32::new(0),
5113 errors_by_kind: HashMap::new(),
5114 errors_by_severity: [const { AtomicU32::new(0) }; 256],
5115 memory_usage: AtomicUsize::new(0),
5116 processing_time: AtomicU32::new(0),
5117 }
5118 }
5119 }
5120
5121 impl ErrorMetrics {
5122 /// Creates a new metrics collector
5123 #[must_use]
5124 pub fn new() -> Self {
5125 Self::default()
5126 }
5127
5128 /// Records an error occurrence
5129 pub fn record_error(&self, error: &Yoshi) {
5130 self.total_errors.fetch_add(1, Ordering::Relaxed);
5131
5132 // Record by severity
5133 let severity = error.severity() as usize;
5134 self.errors_by_severity[severity].fetch_add(1, Ordering::Relaxed);
5135
5136 // Estimate memory usage
5137 let estimated_size = std::mem::size_of_val(error)
5138 + error
5139 .contexts()
5140 .map(|ctx| {
5141 ctx.message.as_ref().map_or(0, |m| m.len()) + ctx.metadata.len() * 64
5142 // Rough estimate
5143 })
5144 .sum::<usize>();
5145
5146 self.memory_usage
5147 .fetch_add(estimated_size, Ordering::Relaxed);
5148 }
5149
5150 /// Gets total error count
5151 #[must_use]
5152 pub fn total_errors(&self) -> u32 {
5153 self.total_errors.load(Ordering::Relaxed)
5154 }
5155
5156 /// Gets errors by severity level
5157 #[must_use]
5158 pub fn errors_by_severity(&self, severity: u8) -> u32 {
5159 self.errors_by_severity[severity as usize].load(Ordering::Relaxed)
5160 }
5161
5162 /// Gets estimated memory usage
5163 #[must_use]
5164 pub fn memory_usage(&self) -> usize {
5165 self.memory_usage.load(Ordering::Relaxed)
5166 }
5167
5168 /// Generates a metrics report
5169 #[must_use]
5170 pub fn generate_report(&self) -> MetricsReport {
5171 MetricsReport {
5172 total_errors: self.total_errors(),
5173 high_severity_errors: (200..=255).map(|s| self.errors_by_severity(s)).sum(),
5174 medium_severity_errors: (100..199).map(|s| self.errors_by_severity(s)).sum(),
5175 low_severity_errors: (0..99).map(|s| self.errors_by_severity(s)).sum(),
5176 memory_usage: self.memory_usage(),
5177 timestamp: SystemTime::now(),
5178 }
5179 }
5180 }
5181 /// Metrics report structure
5182 #[derive(Debug, Clone)]
5183 pub struct MetricsReport {
5184 /// Total number of errors recorded
5185 pub total_errors: u32,
5186 /// Number of high-severity errors
5187 pub high_severity_errors: u32,
5188 /// Number of medium-severity errors
5189 pub medium_severity_errors: u32,
5190 /// Number of low-severity errors
5191 pub low_severity_errors: u32,
5192 /// Current memory usage in bytes
5193 pub memory_usage: usize,
5194 /// Timestamp when the report was generated
5195 pub timestamp: SystemTime,
5196 }
5197
5198 /// Global metrics instance
5199 static GLOBAL_METRICS: OnceLock<ErrorMetrics> = OnceLock::new();
5200
5201 /// Gets the global metrics collector
5202 pub fn global_metrics() -> &'static ErrorMetrics {
5203 GLOBAL_METRICS.get_or_init(ErrorMetrics::new)
5204 }
5205
5206 /// Records an error in global metrics
5207 pub fn record_global_error(error: &Yoshi) {
5208 global_metrics().record_error(error);
5209 }
5210 /// Gets a global metrics report
5211 #[must_use]
5212 pub fn global_report() -> MetricsReport {
5213 global_metrics().generate_report()
5214 }
5215
5216 /// Resets global metrics (primarily for testing)
5217 #[cfg(test)]
5218 pub fn reset_global_metrics() {
5219 // This would require a more sophisticated reset mechanism in production
5220 // For now, we just create a new instance
5221 // Note: This doesn't actually reset the OnceLock, just documents the intention
5222 }
5223}
5224
5225//--------------------------------------------------------------------------------------------------
5226// Comprehensive test suite with performance validation
5227//--------------------------------------------------------------------------------------------------
5228
5229#[cfg(test)]
5230mod tests {
5231 use super::*;
5232 // TypeId is not needed for checking foreign error names after switching to type_name!
5233 // use core::any::TypeId; // For TypeId usage in tests
5234
5235 #[cfg(feature = "std")]
5236 use std::io::ErrorKind;
5237 #[cfg(feature = "std")]
5238 use std::{env, io};
5239
5240 #[test]
5241 fn test_error_instance_counter() {
5242 // Reset counter to ensure test isolation for precise counting
5243 reset_error_instance_counter();
5244
5245 let initial_count = error_instance_count();
5246 let _err1 = Yoshi::new(YoshiKind::Internal {
5247 message: "test".into(),
5248 source: None,
5249 component: None,
5250 });
5251 let after_first_count = error_instance_count();
5252 // Allow for some variance due to potential concurrent test execution
5253 assert!(
5254 after_first_count > initial_count,
5255 "Creating first error should increment counter by at least 1"
5256 );
5257
5258 let _err2 = Yoshi::new(YoshiKind::Internal {
5259 message: "test".into(),
5260 source: None,
5261 component: None,
5262 });
5263 let after_second_count = error_instance_count();
5264 // Creating the second error should also increment by at least 1
5265 assert!(
5266 after_second_count > after_first_count,
5267 "Creating second error should increment counter by at least 1"
5268 );
5269 }
5270
5271 #[test]
5272 fn test_yoshikind_io_display() {
5273 #[cfg(feature = "std")]
5274 {
5275 let io_err = io::Error::new(ErrorKind::NotFound, "file not found");
5276 let kind = YoshiKind::Io(io_err);
5277 assert_eq!(kind.to_string(), "I/O error: file not found");
5278 }
5279 #[cfg(not(feature = "std"))]
5280 {
5281 let kind = YoshiKind::Io(NoStdIo::GenericIo("memory exhausted".into()));
5282 assert_eq!(kind.to_string(), "I/O error (no_std): memory exhausted");
5283 }
5284 }
5285
5286 #[test]
5287 fn test_yoshikind_resource_exhausted_display() {
5288 let kind = YoshiKind::ResourceExhausted {
5289 resource: "memory".into(),
5290 limit: "1GB".into(),
5291 current: "1.2GB".into(),
5292 usage_percentage: Some(120.0),
5293 };
5294 assert_eq!(
5295 kind.to_string(),
5296 "Resource 'memory' exhausted: 1.2GB (limit: 1GB) [120.0% usage]"
5297 );
5298 }
5299
5300 #[test]
5301 fn test_yoshikind_timeout_uses_core_duration() {
5302 let kind = YoshiKind::Timeout {
5303 operation: "long_task".into(),
5304 duration: Duration::from_secs(5),
5305 expected_max: None,
5306 };
5307 assert_eq!(kind.to_string(), "Operation 'long_task' timed out after 5s");
5308 // Verify type is core::time::Duration
5309 let _duration: Duration = match kind {
5310 YoshiKind::Timeout { duration, .. } => duration,
5311 _ => panic!("Expected Timeout variant"),
5312 };
5313 }
5314
5315 #[test]
5316 fn test_from_std_io_error() {
5317 #[cfg(feature = "std")]
5318 {
5319 let io_err = io::Error::new(ErrorKind::NotFound, "file not found");
5320 let yoshi_err = Yoshi::from(io_err);
5321 assert!(format!("{yoshi_err}").contains("I/O error: file not found"));
5322 assert!(matches!(yoshi_err.kind, YoshiKind::Io(_)));
5323 }
5324 #[cfg(not(feature = "std"))]
5325 {
5326 let no_std_io_err = NoStdIo::new("no_std file not found");
5327 let yoshi_err = Yoshi::from(no_std_io_err);
5328 assert!(format!("{yoshi_err}").contains("I/O error (no_std): no_std file not found"));
5329 assert!(matches!(yoshi_err.kind, YoshiKind::Io(_)));
5330 }
5331 }
5332
5333 #[test]
5334 fn test_from_string() {
5335 let msg = "simple string error".to_string();
5336 let yoshi_err = Yoshi::from(msg.clone());
5337 assert!(matches!(
5338 yoshi_err.kind,
5339 YoshiKind::Internal {
5340 ref message, ..
5341 } if message.as_ref() == msg
5342 ));
5343 assert!(format!("{yoshi_err}").contains(&msg));
5344 }
5345
5346 #[test]
5347 fn test_yoshi_foreign_from_boxed_error() {
5348 #[derive(Debug)]
5349 struct MyCustomError;
5350 impl Display for MyCustomError {
5351 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
5352 write!(f, "a custom error occurred")
5353 }
5354 }
5355 impl Error for MyCustomError {}
5356
5357 let boxed_err = Box::new(MyCustomError);
5358 let yoshi_err = Yoshi::foreign(boxed_err); // Changed to Yoshi::foreign
5359 assert!(format!("{yoshi_err}").contains("a custom error occurred"));
5360 assert!(matches!(yoshi_err.kind, YoshiKind::Foreign { .. }));
5361 if let YoshiKind::Foreign {
5362 error_type_name, ..
5363 } = yoshi_err.kind
5364 {
5365 assert_eq!(error_type_name.as_ref(), "alloc::boxed::Box<yoshi_std::tests::test_yoshi_foreign_from_boxed_error::MyCustomError>");
5366 } else {
5367 panic!("Expected Foreign kind");
5368 }
5369 }
5370
5371 #[test]
5372 fn test_contextualization() {
5373 #[cfg(feature = "std")]
5374 let base_err = io::Error::new(ErrorKind::PermissionDenied, "access denied");
5375 #[cfg(not(feature = "std"))]
5376 let base_err = NoStdIo::new("access denied");
5377
5378 let yoshi_err = Yoshi::from(base_err)
5379 .context("Attempted to write to a protected directory".to_string())
5380 .with_metadata("user_id".to_string(), "guest".to_string())
5381 .with_suggestion("Try running with elevated privileges".to_string())
5382 .with_priority(200);
5383
5384 let err_string = format!("{yoshi_err}");
5385 assert!(err_string.contains("access denied"));
5386 assert!(err_string.contains("Caused by: Attempted to write to a protected directory"));
5387 assert!(err_string.contains("user_id: guest"));
5388 assert!(err_string.contains("Suggestion: Try running with elevated privileges"));
5389 assert_eq!(yoshi_err.primary_context().unwrap().priority, 200);
5390 }
5391 #[test]
5392 fn test_chained_yoshi_kind() {
5393 let inner_yoshi = Yoshi::new(YoshiKind::Network {
5394 message: "Connection refused".into(),
5395 source: None,
5396 error_code: None,
5397 });
5398
5399 let outer_yoshi = Yoshi::new(YoshiKind::Internal {
5400 message: "Service communication failed".into(),
5401 source: Some(Box::new(inner_yoshi)),
5402 component: None,
5403 });
5404 let err_string = format!("{outer_yoshi}");
5405
5406 assert!(err_string.contains("Internal error: Service communication failed"));
5407 assert!(err_string.contains("Caused by: Network error: Connection refused")); // Check for nested display
5408 assert!(!err_string.contains("Original Cause: Network error: Connection refused"));
5409 // Should not be duplicated
5410 }
5411
5412 #[test]
5413 #[cfg(feature = "std")]
5414 fn test_backtrace_capture_if_enabled() {
5415 let original_rust_backtrace = env::var("RUST_BACKTRACE").ok();
5416 let original_production_mode = env::var("YOSHI_PRODUCTION_MODE").ok();
5417
5418 // Ensure we're not in production mode which would sanitize the backtrace
5419 env::remove_var("YOSHI_PRODUCTION_MODE");
5420 env::set_var("RUST_BACKTRACE", "1");
5421
5422 let err = Yoshi::new(YoshiKind::Internal {
5423 message: "Test internal error with backtrace".into(),
5424 source: None,
5425 component: None,
5426 });
5427 assert!(err.backtrace().is_some());
5428
5429 let formatted_error = format!("{err}");
5430 // Check for the backtrace framework indicator that's always included
5431 assert!(formatted_error.contains("Generated by std::backtrace framework"));
5432 assert!(formatted_error.contains("Backtrace captured at:"));
5433
5434 // Restore original environment
5435 if let Some(val) = original_rust_backtrace {
5436 env::set_var("RUST_BACKTRACE", val);
5437 } else {
5438 env::remove_var("RUST_BACKTRACE");
5439 }
5440
5441 if let Some(val) = original_production_mode {
5442 env::set_var("YOSHI_PRODUCTION_MODE", val);
5443 }
5444 }
5445
5446 #[test]
5447 fn test_no_backtrace_if_disabled() {
5448 #[cfg(feature = "std")]
5449 let original_rust_backtrace = env::var("RUST_BACKTRACE").ok();
5450 #[cfg(feature = "std")]
5451 env::remove_var("RUST_BACKTRACE");
5452
5453 let err = Yoshi::new(YoshiKind::Internal {
5454 message: "No backtrace expected".into(),
5455 source: None,
5456 component: None,
5457 });
5458
5459 #[cfg(feature = "std")]
5460 assert!(err.backtrace().is_none());
5461 #[cfg(not(feature = "std"))]
5462 assert!(err.backtrace.is_none());
5463
5464 assert!(!format!("{err}").contains("stack backtrace"));
5465
5466 #[cfg(feature = "std")]
5467 {
5468 if let Some(val) = original_rust_backtrace {
5469 env::set_var("RUST_BACKTRACE", val);
5470 }
5471 }
5472 }
5473
5474 #[test]
5475 fn test_access_metadata_directly() {
5476 let err = Yoshi::new(YoshiKind::Internal {
5477 message: "Test provide metadata".into(),
5478 source: None,
5479 component: None,
5480 })
5481 .with_metadata("id", "123")
5482 .with_metadata("status", "failed");
5483
5484 // Access metadata directly from the YoContext
5485 let ctx = err
5486 .primary_context()
5487 .expect("Should have a primary context");
5488 let map = &ctx.metadata;
5489 assert_eq!(map.get(&Arc::from("id")), Some(&Arc::from("123")));
5490 assert_eq!(map.get(&Arc::from("status")), Some(&Arc::from("failed")));
5491 }
5492
5493 #[test]
5494 fn test_yoshi_location_macro() {
5495 let loc = yoshi_location!();
5496 assert!(loc.file.ends_with("lib.rs"));
5497 assert!(loc.line > 0);
5498 assert!(loc.column > 0);
5499 assert_eq!(
5500 format!("{loc}"),
5501 format!("{}:{}:{}", loc.filename(), loc.line, loc.column)
5502 );
5503 }
5504
5505 #[test]
5506 fn test_yoshi_with_payload_and_access() {
5507 #[derive(Debug, PartialEq)]
5508 struct CustomErrorPayload {
5509 code: u16,
5510 message: String,
5511 }
5512 impl Display for CustomErrorPayload {
5513 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
5514 write!(f, "CustomPayload: code={}, msg={}", self.code, self.message)
5515 }
5516 }
5517
5518 let err = Yoshi::new(YoshiKind::Internal {
5519 message: "Operation failed with custom shell".into(),
5520 source: None,
5521 component: None,
5522 })
5523 .with_shell(CustomErrorPayload {
5524 code: 500,
5525 message: "Internal server error details".into(),
5526 })
5527 .with_shell("a string shell".to_string())
5528 .with_shell(42u32);
5529
5530 // Access payloads using the more robust `Yoshi::shell` method
5531 let ctx = err
5532 .primary_context()
5533 .expect("Should have a primary context");
5534
5535 let custom_payload = ctx.shell::<CustomErrorPayload>();
5536 assert!(custom_payload.is_some());
5537 assert_eq!(custom_payload.unwrap().code, 500);
5538
5539 let string_payload = ctx.shell::<String>();
5540 assert!(string_payload.is_some());
5541 assert_eq!(string_payload.unwrap(), &"a string shell".to_string());
5542
5543 let u32_payload = ctx.shell::<u32>();
5544 assert!(u32_payload.is_some());
5545 assert_eq!(*u32_payload.unwrap(), 42);
5546 }
5547
5548 #[test]
5549 fn test_yoshi_context_ext_with_payload_on_result() {
5550 #[derive(Debug, PartialEq)]
5551 struct TransactionId(String);
5552
5553 #[cfg(feature = "std")]
5554 let result: std::result::Result<u32, std::io::Error> = Err(io::Error::new(
5555 ErrorKind::PermissionDenied,
5556 "db write failed",
5557 ));
5558 #[cfg(not(feature = "std"))]
5559 let result: core::result::Result<u32, NoStdIo> = Err(NoStdIo::new("db write failed"));
5560
5561 let yoshi_result = result
5562 .with_shell(TransactionId("tx123".into()))
5563 .context("Failed to commit transaction".to_string());
5564
5565 assert!(yoshi_result.is_err());
5566 let err = yoshi_result.unwrap_err();
5567
5568 assert!(format!("{err}").contains("db write failed"));
5569 assert!(format!("{err}").contains("Caused by: Failed to commit transaction")); // Access shell using the corrected `Yoshi::shell` method that searches all contexts
5570 let transaction_id = err.shell::<TransactionId>();
5571
5572 assert!(transaction_id.is_some(), "Should find TransactionId shell");
5573 assert_eq!(transaction_id.unwrap().0, "tx123".to_string());
5574 }
5575
5576 #[test]
5577 fn test_yoshi_context_ext_short_aliases() {
5578 #[cfg(feature = "std")]
5579 let result: std::result::Result<(), std::io::Error> = Err(io::Error::new(
5580 io::ErrorKind::NotFound,
5581 "file.txt not found",
5582 ));
5583 #[cfg(not(feature = "std"))]
5584 let result: core::result::Result<(), NoStdIo> = Err(NoStdIo::NotFound);
5585
5586 let err = result
5587 .ctx("Failed to open file".to_string())
5588 .help("Check file path and permissions".to_string())
5589 .meta("file_name".to_string(), "file.txt".to_string())
5590 .unwrap_err();
5591
5592 let s = format!("{err}");
5593 assert!(s.contains("Failed to open file"));
5594 assert!(s.contains("Check file path and permissions"));
5595 assert!(s.contains("file_name: file.txt"));
5596 }
5597
5598 #[test]
5599 fn test_hatch_type_alias() {
5600 let success: Hatch<u32> = Ok(42);
5601 if let Ok(value) = success {
5602 assert_eq!(value, 42);
5603 } else {
5604 panic!("Expected Ok value");
5605 }
5606
5607 let failure: Hatch<u32> = Err(Yoshi::new(YoshiKind::Internal {
5608 message: "test error".into(),
5609 source: None,
5610 component: None,
5611 }));
5612 assert!(failure.is_err());
5613 }
5614
5615 #[test]
5616 fn test_lay_context_trait() {
5617 let error = Yoshi::new(YoshiKind::Internal {
5618 message: "base error".into(),
5619 source: None,
5620 component: None,
5621 });
5622
5623 let result: Hatch<()> = Err(error).lay("additional context");
5624 assert!(result.is_err());
5625
5626 let err = result.unwrap_err();
5627 assert!(err.to_string().contains("additional context"));
5628 }
5629
5630 #[test]
5631 fn test_hatchable_trait() {
5632 #[cfg(feature = "std")]
5633 {
5634 use std::io;
5635 let io_result: Result<String, io::Error> =
5636 Err(io::Error::new(io::ErrorKind::NotFound, "file not found"));
5637 let hatched = io_result.hatch();
5638 assert!(hatched.is_err());
5639 }
5640
5641 let string_result: Result<i32, String> = Err("conversion failed".to_string());
5642 let hatched = string_result.hatch();
5643 assert!(hatched.is_err());
5644 }
5645
5646 #[test]
5647 fn test_yoshi_enhanced_methods() {
5648 let error = Yoshi::new(YoshiKind::Internal {
5649 message: "base error".into(),
5650 source: None,
5651 component: None,
5652 })
5653 .lay("operation context");
5654
5655 // Test laytext method
5656 assert_eq!(error.laytext().unwrap(), "operation context");
5657
5658 // Test nest method (should be None for this error)
5659 assert!(error.nest().is_none());
5660 }
5661
5662 #[test]
5663 fn test_yum_macro() {
5664 let error = Yoshi::new(YoshiKind::Internal {
5665 message: "test error for yum".into(),
5666 source: None,
5667 component: None,
5668 })
5669 .context("test context")
5670 .with_suggestion("try again");
5671
5672 // yum! macro should not panic and should return the error
5673 let returned_error = yum!(error);
5674 assert_eq!(returned_error.laytext().unwrap(), "test context");
5675 assert_eq!(returned_error.suggestion().unwrap(), "try again");
5676 }
5677
5678 #[test]
5679 fn test_hatch_backwards_compatibility() {
5680 use core::error::Error;
5681
5682 let error = Yoshi::new(YoshiKind::Internal {
5683 message: "compatibility test".into(),
5684 source: None,
5685 component: None,
5686 });
5687
5688 // Test that standard Error trait methods still work
5689 let error_ref: &dyn Error = &error;
5690 assert!(error_ref.source().is_none());
5691
5692 // Test that new methods work alongside old ones
5693 assert!(error.nest().is_none()); // New method
5694 assert!(error.laytext().is_none()); // New method (no context added)
5695 }
5696
5697 #[test]
5698 fn test_hatch_ecosystem_integration() {
5699 // Test complete workflow with all Hatch ecosystem components
5700 fn complex_operation() -> Hatch<u32> {
5701 #[cfg(feature = "std")]
5702 let io_result: Result<String, std::io::Error> = Err(std::io::Error::new(
5703 std::io::ErrorKind::PermissionDenied,
5704 "access denied",
5705 ));
5706 #[cfg(not(feature = "std"))]
5707 let io_result: Result<String, NoStdIo> = Err(NoStdIo::PermissionDenied);
5708
5709 io_result
5710 .hatch()
5711 .lay("while accessing configuration")
5712 .context("during system initialization")
5713 .map_err(|e| {
5714 e.with_metadata("component", "config_loader")
5715 .with_suggestion("check file permissions")
5716 })?;
5717
5718 Ok(42)
5719 }
5720
5721 let result = complex_operation();
5722 assert!(result.is_err());
5723
5724 let error = result.unwrap_err();
5725
5726 // Verify thematic methods work
5727 assert!(error.laytext().is_some());
5728 assert_eq!(error.suggestion().unwrap(), "check file permissions");
5729
5730 // Verify nest access works
5731 assert!(error.nest().is_some());
5732
5733 // Test yum! macro
5734 let debug_error = yum!(error);
5735 assert_eq!(debug_error.instance_id(), error.instance_id());
5736 }
5737}