#![allow(dead_code)]
#![warn(clippy::all)]
#![warn(missing_docs)]
#![warn(clippy::cargo)]
#![warn(clippy::pedantic)]
#![allow(unexpected_cfgs)] #![allow(clippy::too_many_lines)] #![allow(clippy::result_large_err)] #![allow(clippy::enum_variant_names)] #![allow(clippy::items_after_statements)] #![allow(clippy::module_name_repetitions)] #![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(feature = "serde")]
mod serde_helpers {
use super::String;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::sync::Arc;
#[allow(clippy::ref_option)]
pub fn serialize_arc_str<S>(value: &Option<Arc<str>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
value
.as_ref()
.map(std::convert::AsRef::as_ref)
.serialize(serializer)
}
pub fn deserialize_arc_str<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
where
D: Deserializer<'de>,
{
let opt_string: Option<String> = Option::deserialize(deserializer)?;
Ok(opt_string.map(|s| Arc::from(s.as_str())))
}
pub fn serialize_arc_str_map<S>(
value: &HashMap<Arc<str>, Arc<str>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let string_map: HashMap<&str, &str> = value
.iter()
.map(|(k, v)| (k.as_ref(), v.as_ref()))
.collect();
string_map.serialize(serializer)
}
pub fn deserialize_arc_str_map<'de, D>(
deserializer: D,
) -> Result<HashMap<Arc<str>, Arc<str>>, D::Error>
where
D: Deserializer<'de>,
{
let string_map: HashMap<String, String> = HashMap::deserialize(deserializer)?;
Ok(string_map
.into_iter()
.map(|(k, v)| (Arc::from(k.as_str()), Arc::from(v.as_str())))
.collect())
}
}
#[cfg(feature = "serde")]
use serde_helpers::{
deserialize_arc_str, deserialize_arc_str_map, serialize_arc_str, serialize_arc_str_map,
};
#[cfg(all(docsrs, feature = "unstable-metrics"))]
compile_error!("unstable-metrics feature is not supported on docs.rs builds");
#[cfg(all(docsrs, feature = "unstable-auto-fix"))]
compile_error!("unstable-auto-fix feature is not supported on docs.rs builds");
#[cfg(all(docsrs, feature = "unstable-smart-diagnostics"))]
compile_error!("unstable-smart-diagnostics feature is not supported on docs.rs builds");
#[allow(unused_macros)] macro_rules! detect_docs_rs {
() => {
cfg!(docsrs) || std::env::var("DOCS_RS").is_ok()
};
}
#[cfg(docsrs)]
mod docs_compatibility {
pub mod fallbacks {
use crate::{Arc, HashMap};
pub type MetricsMap = HashMap<Arc<str>, Arc<str>>;
pub type AsyncResult<T> = core::future::Ready<Result<T, crate::Yoshi>>;
#[cfg(feature = "serde")]
pub mod serde_docs {
pub fn safe_serialize() -> &'static str {
"Serialization available in runtime builds"
}
}
}
}
#[cfg(not(docsrs))]
mod runtime_impl {
#[allow(unused_imports)] pub mod runtime {
pub use crate::*;
}
}
#[cfg(not(feature = "std"))]
pub use alloc::{
boxed::Box,
format, string::{String, ToString},
sync::Arc,
vec, vec::Vec,
};
#[cfg(not(feature = "std"))]
#[allow(unused_macros)] macro_rules! eprintln {
() => {};
($($arg:tt)*) => {
};
}
#[cfg(feature = "std")]
pub use std::{
boxed::Box,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use core::any::Any; use core::error::Error; use core::fmt::{self, Display, Formatter};
use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use core::time::Duration;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as HashMap;
#[cfg(feature = "std")]
use std::collections::HashMap; #[cfg(not(feature = "std"))]
use core::sync::atomic::AtomicU64;
#[cfg(feature = "std")]
use std::{thread, time::SystemTime}; #[cfg(not(feature = "std"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SystemTime {
timestamp: u64,
}
#[cfg(not(feature = "std"))]
impl SystemTime {
pub fn now() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
Self {
timestamp: COUNTER.fetch_add(1, Ordering::Relaxed),
}
}
pub const fn timestamp(&self) -> u64 {
self.timestamp
}
pub const fn duration_since(&self, earlier: SystemTime) -> Option<u64> {
if self.timestamp >= earlier.timestamp {
Some(self.timestamp - earlier.timestamp)
} else {
None
}
}
pub fn elapsed(&self) -> u64 {
Self::now().timestamp.saturating_sub(self.timestamp)
}
}
#[cfg(not(feature = "std"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ThreadId {
id: u32,
}
#[cfg(not(feature = "std"))]
impl ThreadId {
pub fn current() -> Self {
static THREAD_COUNTER: AtomicU32 = AtomicU32::new(1); #[cfg(all(target_has_atomic = "ptr", feature = "std"))]
{
use core::cell::Cell;
std::thread_local! {
static THREAD_ID: Cell<Option<u32>> = const { Cell::new(None) };
}
THREAD_ID.with(|id| {
let current_id = id.get().unwrap_or_else(|| {
let new_id = THREAD_COUNTER.fetch_add(1, Ordering::Relaxed);
id.set(Some(new_id));
new_id
});
Self { id: current_id }
})
}
#[cfg(not(all(target_has_atomic = "ptr", feature = "std")))]
{
Self {
id: THREAD_COUNTER.fetch_add(1, Ordering::Relaxed),
}
}
}
#[inline]
pub const fn as_u32(&self) -> u32 {
self.id
}
pub const fn from_u32(id: u32) -> Self {
Self { id }
}
}
#[cfg(not(feature = "std"))]
impl core::fmt::Display for ThreadId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "ThreadId({})", self.id)
}
}
#[cfg(not(feature = "std"))]
use core::cell::UnsafeCell;
#[cfg(not(feature = "std"))]
use core::sync::atomic::AtomicBool;
#[cfg(feature = "std")]
use std::sync::OnceLock;
#[cfg(not(feature = "std"))]
pub struct OnceLock<T> {
initialized: AtomicBool,
data: UnsafeCell<Option<T>>,
}
#[cfg(not(feature = "std"))]
unsafe impl<T: Send + Sync> Sync for OnceLock<T> {}
#[cfg(not(feature = "std"))]
unsafe impl<T: Send> Send for OnceLock<T> {}
#[cfg(not(feature = "std"))]
impl<T> OnceLock<T> {
pub const fn new() -> Self {
Self {
initialized: AtomicBool::new(false),
data: UnsafeCell::new(None),
}
}
pub fn get_or_init(&self, f: impl FnOnce() -> T) -> &T {
if self
.initialized
.compare_exchange_weak(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let value = f();
unsafe {
let data_ptr = self.data.get();
*data_ptr = Some(value);
}
} else {
while !self.initialized.load(Ordering::Acquire) {
core::hint::spin_loop();
}
}
unsafe {
let data_ptr = self.data.get();
(*data_ptr).as_ref().unwrap_unchecked()
}
}
pub fn get(&self) -> Option<&T> {
if self.initialized.load(Ordering::Acquire) {
unsafe {
let data_ptr = self.data.get();
(*data_ptr).as_ref()
}
} else {
None
}
}
}
#[derive(Debug)]
struct ForeignErrorWrapper {
inner: Box<dyn Error + Send + Sync + 'static>,
context: String,
enhanced_message: String,
}
impl Display for ForeignErrorWrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.context.is_empty() {
write!(f, "{}", self.enhanced_message)
} else {
write!(f, "{}: {}", self.context, self.enhanced_message)
}
}
}
impl Error for ForeignErrorWrapper {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.inner.as_ref())
}
}
pub struct OptimizedFormatBuffer {
data: String,
reserved_capacity: usize,
}
impl OptimizedFormatBuffer {
const DEFAULT_CAPACITY: usize = 2048;
#[must_use]
pub fn new() -> Self {
Self {
data: String::with_capacity(Self::DEFAULT_CAPACITY),
reserved_capacity: Self::DEFAULT_CAPACITY,
}
}
}
impl Default for OptimizedFormatBuffer {
fn default() -> Self {
Self::new()
}
}
impl OptimizedFormatBuffer {
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
data: String::with_capacity(capacity),
reserved_capacity: capacity,
}
}
pub fn append_optimized(&mut self, s: &str) {
let current_len = self.data.len();
let append_len = s.len();
let new_len = current_len + append_len;
if new_len > self.data.capacity() {
let current_cap = self.data.capacity();
let min_growth_needed = self.reserved_capacity.max(256);
let growth_target_1_5x = current_cap + (current_cap >> 1); let new_capacity = growth_target_1_5x.max(new_len).max(min_growth_needed);
self.data.reserve(new_capacity - current_cap);
}
self.data.push_str(s);
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.data
}
pub fn clear(&mut self) {
self.data.clear();
}
pub fn append_multiple(&mut self, fragments: &[&str]) {
let total_len: usize = fragments.iter().map(|s| s.len()).sum();
let new_len = self.data.len() + total_len;
if new_len > self.data.capacity() {
let new_capacity = (new_len * 2)
.next_power_of_two()
.max(self.reserved_capacity);
self.data.reserve_exact(new_capacity - self.data.capacity());
}
for fragment in fragments {
self.data.push_str(fragment);
}
}
}
#[derive(Debug, Clone)]
pub enum ErrorRecoveryStrategy {
ExponentialBackoff {
initial_delay: Duration,
max_retries: u32,
backoff_multiplier: f64,
},
FixedInterval {
interval: Duration,
max_retries: u32,
},
Fallback {
description: String,
},
CircuitBreaker {
failure_threshold: u32,
recovery_timeout: Duration,
},
NonRecoverable,
}
#[derive(Debug, Default)]
pub struct ContextAnalysis {
pub total_contexts: usize,
pub context_depth: usize,
pub has_suggestions: bool,
pub has_location_info: bool,
pub metadata_entries: usize,
pub typed_payloads: usize,
pub primary_context_priority: u8,
}
#[cfg(feature = "std")]
pub type Result<T, E = Yoshi> = std::result::Result<T, E>;
#[cfg(not(feature = "std"))]
pub type Result<T, E = Yoshi> = core::result::Result<T, E>;
pub type Hatch<T> = Result<T, Yoshi>;
static ERROR_INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(0);
static STRING_INTERN_POOL: OnceLock<StringInternPool> = OnceLock::new();
#[allow(unexpected_cfgs)] #[cfg(all(docsrs, any(feature = "simd-optimized", feature = "precise-capturing")))]
mod nightly_workarounds {
#[cfg(feature = "simd-optimized")]
pub fn safe_simd_placeholder() -> &'static str {
"SIMD optimizations disabled for docs.rs compatibility"
}
#[cfg(feature = "precise-capturing")]
pub fn safe_capturing_placeholder() -> &'static str {
"Precise capturing disabled for docs.rs compatibility"
}
}
#[inline]
fn is_production_mode() -> bool {
#[cfg(feature = "std")]
{
std::env::var("YOSHI_PRODUCTION_MODE")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
#[cfg(not(feature = "std"))]
{
false }
}
fn sanitize_error_message(msg: &str) -> String {
const MAX_MESSAGE_LENGTH: usize = 256;
let mut sanitized = msg.to_string();
let lower_msg = msg.to_lowercase();
if lower_msg.contains("password") {
sanitized = sanitized.replace("password", "password=[REDACTED]");
}
if lower_msg.contains("token") {
sanitized = sanitized.replace("token", "token=[REDACTED]");
}
if lower_msg.contains("key") {
sanitized = sanitized.replace("key", "key=[REDACTED]");
}
if sanitized.len() > MAX_MESSAGE_LENGTH {
sanitized.truncate(MAX_MESSAGE_LENGTH);
sanitized.push_str("... [truncated]");
}
sanitized
}
struct StringInternPool {
#[cfg(feature = "std")]
pool: std::sync::RwLock<std::collections::HashMap<String, Arc<str>>>,
#[cfg(not(feature = "std"))]
pool: alloc::collections::BTreeMap<String, Arc<str>>,
hits: AtomicUsize,
misses: AtomicUsize,
cache_size: AtomicUsize,
}
impl StringInternPool {
fn new() -> Self {
Self {
#[cfg(feature = "std")]
pool: std::sync::RwLock::new(std::collections::HashMap::with_capacity(128)),
#[cfg(not(feature = "std"))]
pool: alloc::collections::BTreeMap::new(),
hits: AtomicUsize::new(0),
misses: AtomicUsize::new(0),
cache_size: AtomicUsize::new(0),
}
}
#[cfg(feature = "std")]
pub fn clear_pool(&self) {
if let Ok(mut pool) = self.pool.write() {
pool.clear();
self.cache_size.store(0, Ordering::Release);
}
}
fn intern(&self, s: impl Into<String>) -> Arc<str> {
let string = s.into();
if string.is_empty() {
return Arc::from("");
}
#[cfg(feature = "std")]
{
if let Ok(pool) = self.pool.try_read() {
if let Some(interned) = pool.get(&string) {
self.hits.fetch_add(1, Ordering::Relaxed);
return interned.clone();
}
}
const MAX_CACHE_SIZE: usize = 512;
let current_size = self.cache_size.load(Ordering::Relaxed);
if current_size >= MAX_CACHE_SIZE {
self.misses.fetch_add(1, Ordering::Relaxed);
return string.into();
}
let mut pool = self
.pool
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(interned) = pool.get(&string) {
self.hits.fetch_add(1, Ordering::Relaxed);
return interned.clone();
}
let current_pool_size = pool.len();
if current_pool_size < MAX_CACHE_SIZE {
let arc_str: Arc<str> = string.as_str().into();
pool.insert(string, arc_str.clone());
self.cache_size
.store(current_pool_size + 1, Ordering::Release);
self.misses.fetch_add(1, Ordering::Relaxed);
arc_str
} else {
self.cache_size.store(current_pool_size, Ordering::Release);
self.misses.fetch_add(1, Ordering::Relaxed);
string.into()
}
}
#[cfg(not(feature = "std"))]
{
use core::ptr;
use core::sync::atomic::AtomicPtr;
const CACHE_SLOTS: usize = 256; static CACHE: [AtomicPtr<CacheEntry>; CACHE_SLOTS] =
[const { AtomicPtr::new(ptr::null_mut()) }; CACHE_SLOTS];
const MAX_GLOBAL_CACHE_SIZE: usize = 512;
#[repr(C)]
struct CacheEntry {
hash: u64,
arc_str: Arc<str>,
next: AtomicPtr<CacheEntry>,
}
#[inline(always)] fn fast_hash(s: &str) -> u64 {
let mut hash = 0xcbf29ce484222325u64; for byte in s.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3u64); }
hash
}
let hash = fast_hash(&string);
let slot_index = (hash as usize) & (CACHE_SLOTS - 1);
let mut current = CACHE[slot_index].load(Ordering::Acquire);
while !current.is_null() {
unsafe {
let entry = &*current;
if entry.hash == hash && entry.arc_str.as_ref() == string {
self.hits.fetch_add(1, Ordering::Relaxed);
return entry.arc_str.clone();
}
current = entry.next.load(Ordering::Acquire);
}
}
let new_cache_size = self.cache_size.fetch_add(1, Ordering::Relaxed) + 1;
if new_cache_size > MAX_GLOBAL_CACHE_SIZE {
self.cache_size.fetch_sub(1, Ordering::Relaxed); self.misses.fetch_add(1, Ordering::Relaxed);
return string.into(); }
let string_for_comparison = string.clone();
let arc_str: Arc<str> = string.into(); let new_entry = Box::into_raw(Box::new(CacheEntry {
hash,
arc_str: arc_str.clone(),
next: AtomicPtr::new(ptr::null_mut()),
}));
let mut head = CACHE[slot_index].load(Ordering::Acquire);
loop {
unsafe {
(*new_entry).next.store(head, Ordering::Relaxed);
}
match CACHE[slot_index].compare_exchange_weak(
head,
new_entry,
Ordering::Release,
Ordering::Acquire,
) {
Ok(_) => {
self.misses.fetch_add(1, Ordering::Relaxed);
return arc_str;
}
Err(current_head) => {
head = current_head; let mut search_current = head;
while !search_current.is_null() {
unsafe {
let entry = &*search_current;
if entry.hash == hash
&& entry.arc_str.as_ref() == string_for_comparison
{
let _ = Box::from_raw(new_entry); self.hits.fetch_add(1, Ordering::Relaxed);
self.cache_size.fetch_sub(1, Ordering::Relaxed); return entry.arc_str.clone();
}
search_current = entry.next.load(Ordering::Acquire);
}
}
}
}
}
}
}
#[inline]
pub fn stats(&self) -> (usize, usize) {
(
self.hits.load(Ordering::Relaxed),
self.misses.load(Ordering::Relaxed),
)
}
#[inline]
pub fn cache_size(&self) -> usize {
self.cache_size.load(Ordering::Acquire)
}
}
#[inline]
pub fn intern_string(s: impl Into<String>) -> Arc<str> {
STRING_INTERN_POOL
.get_or_init(StringInternPool::new)
.intern(s)
}
pub fn error_instance_count() -> u32 {
ERROR_INSTANCE_COUNTER.load(Ordering::Relaxed)
}
#[cfg(test)]
#[inline]
pub fn reset_error_instance_counter() {
ERROR_INSTANCE_COUNTER.store(0, Ordering::Relaxed);
}
#[cfg(not(feature = "std"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NoStdIoKind {
NotFound,
PermissionDenied,
ConnectionRefused,
TimedOut,
Generic,
Other,
}
#[cfg(not(feature = "std"))]
impl NoStdIoKind {
pub const fn as_str(&self) -> &'static str {
match self {
Self::NotFound => "not found",
Self::PermissionDenied => "permission denied",
Self::ConnectionRefused => "connection refused",
Self::TimedOut => "timed out",
Self::Generic => "I/O error",
Self::Other => "other error",
}
}
pub const fn is_transient(&self) -> bool {
matches!(
self,
Self::ConnectionRefused | Self::TimedOut | Self::Generic
)
}
pub const fn severity(&self) -> u8 {
match self {
Self::NotFound => 30,
Self::PermissionDenied => 50,
Self::ConnectionRefused => 40,
Self::TimedOut => 35,
Self::Generic => 45,
Self::Other => 40,
}
}
}
#[cfg(not(feature = "std"))]
impl core::fmt::Display for NoStdIoKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(not(feature = "std"))]
#[derive(Debug, Clone)]
pub enum NoStdIo {
GenericIo(Arc<str>),
NotFound,
PermissionDenied,
ConnectionRefused,
TimedOut,
Other(Arc<str>),
}
#[cfg(not(feature = "std"))]
impl NoStdIo {
pub fn new(message: impl Into<String>) -> Self {
let msg = message.into();
let lower_msg = msg.to_lowercase();
match lower_msg.as_str() {
s if s.contains("not found")
|| s.contains("no such file")
|| s.contains("enoent")
|| s.contains("file does not exist") =>
{
Self::NotFound
}
s if s.contains("permission denied")
|| s.contains("access denied")
|| s.contains("access is denied")
|| s.contains("eacces")
|| s.contains("unauthorized")
|| s.contains("forbidden") =>
{
Self::PermissionDenied
}
s if s.contains("connection refused")
|| s.contains("econnrefused")
|| s.contains("no route to host")
|| s.contains("network unreachable") =>
{
Self::ConnectionRefused
}
s if s.contains("timed out")
|| s.contains("timeout")
|| s.contains("etimedout")
|| s.contains("operation timeout") =>
{
Self::TimedOut
}
s if s.contains("i/o error")
|| s.contains("io error")
|| s.contains("input/output error") =>
{
Self::GenericIo(msg.into())
}
_ => Self::Other(msg.into()),
}
}
pub fn from_code_and_message(code: i32, message: impl Into<String>) -> Self {
match code {
2 | -2 => Self::NotFound, 13 | -13 => Self::PermissionDenied, 111 | -111 => Self::ConnectionRefused, 110 | -110 => Self::TimedOut, 5 | -5 => Self::GenericIo(message.into().into()), _ => Self::Other(message.into().into()),
}
}
pub fn typed_error(error_type: NoStdIoKind, message: impl Into<String>) -> Self {
match error_type {
NoStdIoKind::NotFound => Self::NotFound,
NoStdIoKind::PermissionDenied => Self::PermissionDenied,
NoStdIoKind::ConnectionRefused => Self::ConnectionRefused,
NoStdIoKind::TimedOut => Self::TimedOut,
NoStdIoKind::Generic => Self::GenericIo(message.into().into()),
NoStdIoKind::Other => Self::Other(message.into().into()),
}
}
}
#[cfg(not(feature = "std"))]
impl Display for NoStdIo {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Self::GenericIo(s) => write!(f, "I/O error (no_std): {s}"),
Self::NotFound => f.write_str("I/O error (no_std): not found"),
Self::PermissionDenied => f.write_str("I/O error (no_std): permission denied"),
Self::ConnectionRefused => f.write_str("I/O error (no_std): connection refused"),
Self::TimedOut => f.write_str("I/O error (no_std): timed out"),
Self::Other(s) => write!(f, "I/O error (no_std): {s}"),
}
}
}
#[cfg(not(feature = "std"))]
impl Error for NoStdIo {}
#[derive(Debug)]
#[non_exhaustive]
pub enum YoshiKind {
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
Io(std::io::Error),
#[cfg(not(feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(not(feature = "std"))))]
Io(NoStdIo),
Network {
message: Arc<str>,
source: Option<Box<Yoshi>>,
error_code: Option<u32>,
},
Config {
message: Arc<str>,
source: Option<Box<Yoshi>>,
config_path: Option<Arc<str>>,
},
Validation {
field: Arc<str>,
message: Arc<str>,
expected: Option<Arc<str>>,
actual: Option<Arc<str>>,
},
Internal {
message: Arc<str>,
source: Option<Box<Yoshi>>,
component: Option<Arc<str>>,
},
NotFound {
resource_type: Arc<str>,
identifier: Arc<str>,
search_locations: Option<Vec<Arc<str>>>,
},
Timeout {
operation: Arc<str>,
duration: Duration,
expected_max: Option<Duration>,
},
ResourceExhausted {
resource: Arc<str>,
limit: Arc<str>,
current: Arc<str>,
usage_percentage: Option<f64>,
},
Foreign {
error: Box<dyn Error + Send + Sync + 'static>,
error_type_name: Arc<str>,
},
Multiple {
errors: Vec<Yoshi>,
primary_index: Option<usize>,
},
}
impl YoshiKind {
pub fn from_foreign_with_context<E>(error: E, context: impl Into<String>) -> Self
where
E: Error + Send + Sync + 'static,
{
let type_name = core::any::type_name::<E>();
let error_msg = error.to_string();
let enhanced_msg = sanitize_error_message(&error_msg);
Self::Foreign {
error: Box::new(ForeignErrorWrapper {
inner: Box::new(error),
context: context.into(),
enhanced_message: enhanced_msg,
}),
error_type_name: intern_string(type_name),
}
}
#[must_use]
pub const fn severity(&self) -> u8 {
match self {
#[cfg(feature = "std")]
Self::Io(_) => 40,
#[cfg(not(feature = "std"))]
Self::Io(_) => 40,
Self::Network { .. } => 50,
Self::Config { .. } => 30,
Self::Validation { .. } => 20,
Self::Internal { .. } => 80,
Self::NotFound { .. } => 25,
Self::Timeout { .. } => 45,
Self::ResourceExhausted { .. } => 70,
Self::Foreign { .. } => 60,
Self::Multiple { .. } => 65,
}
}
#[must_use]
pub const fn is_transient(&self) -> bool {
matches!(
self,
Self::Network { .. } | Self::Timeout { .. } | Self::ResourceExhausted { .. }
)
}
}
impl Clone for YoshiKind {
fn clone(&self) -> Self {
match self {
#[cfg(feature = "std")]
Self::Io(e) => {
Self::Io(std::io::Error::new(e.kind(), e.to_string()))
}
#[cfg(not(feature = "std"))]
Self::Io(e) => Self::Io(e.clone()),
Self::Network {
message,
source,
error_code,
} => Self::Network {
message: message.clone(),
source: source.clone(),
error_code: *error_code,
},
Self::Config {
message,
source,
config_path,
} => Self::Config {
message: message.clone(),
source: source.clone(),
config_path: config_path.clone(),
},
Self::Validation {
field,
message,
expected,
actual,
} => Self::Validation {
field: field.clone(),
message: message.clone(),
expected: expected.clone(),
actual: actual.clone(),
},
Self::Internal {
message,
source,
component,
} => Self::Internal {
message: message.clone(),
source: source.clone(),
component: component.clone(),
},
Self::NotFound {
resource_type,
identifier,
search_locations,
} => Self::NotFound {
resource_type: resource_type.clone(),
identifier: identifier.clone(),
search_locations: search_locations.clone(),
},
Self::Timeout {
operation,
duration,
expected_max,
} => Self::Timeout {
operation: operation.clone(),
duration: *duration,
expected_max: *expected_max,
},
Self::ResourceExhausted {
resource,
limit,
current,
usage_percentage,
} => Self::ResourceExhausted {
resource: resource.clone(),
limit: limit.clone(),
current: current.clone(),
usage_percentage: *usage_percentage,
},
Self::Foreign {
error,
error_type_name,
} => {
Self::Internal {
message: format!("Cloned foreign error: {error}").into(),
source: None,
component: Some(format!("Originally: {error_type_name}").into()),
}
}
Self::Multiple {
errors,
primary_index,
} => Self::Multiple {
errors: errors.clone(),
primary_index: *primary_index,
},
}
}
}
impl Display for YoshiKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "std")]
Self::Io(e) => write!(f, "I/O error: {e}"),
#[cfg(not(feature = "std"))]
Self::Io(e) => write!(f, "{e}"),
Self::Network {
message,
error_code,
..
} => {
if let Some(code) = error_code {
write!(f, "Network error (code {code}): {message}")
} else {
write!(f, "Network error: {message}")
}
}
Self::Config {
message,
config_path,
..
} => {
if let Some(path) = config_path.as_ref() {
write!(f, "Configuration error in '{path}': {message}")
} else {
write!(f, "Configuration error: {message}")
}
}
Self::Validation {
field,
message,
expected,
actual,
} => {
write!(f, "Validation error for '{field}': {message}")?;
if let (Some(exp), Some(act)) = (expected, actual) {
write!(f, " (expected: {exp}, actual: {act})")?;
}
Ok(())
}
Self::Internal {
message, component, ..
} => {
if let Some(comp) = component.as_ref() {
write!(f, "Internal error in {comp}: {message}")
} else {
write!(f, "Internal error: {message}")
}
}
Self::NotFound {
resource_type,
identifier,
..
} => write!(f, "{resource_type} not found: {identifier}"),
Self::Timeout {
operation,
duration,
expected_max,
} => {
write!(f, "Operation '{operation}' timed out after {duration:?}")?;
if let Some(max) = expected_max {
write!(f, " (max expected: {max:?})")?;
}
Ok(())
}
Self::ResourceExhausted {
resource,
limit,
current,
usage_percentage,
} => {
write!(
f,
"Resource '{resource}' exhausted: {current} (limit: {limit})"
)?;
if let Some(pct) = usage_percentage {
write!(f, " [{pct:.1}% usage]")?;
}
Ok(())
}
Self::Foreign {
error,
error_type_name,
} => {
write!(f, "{error_type_name}: {error}")
}
Self::Multiple {
errors,
primary_index,
} => {
let primary = primary_index.and_then(|i| errors.get(i)); write!(f, "Multiple errors ({} total)", errors.len())?;
if let Some(primary_err) = primary {
write!(f, " - Primary: {primary_err}")?;
}
Ok(())
}
}
}
}
impl YoshiKind {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
#[cfg(feature = "std")]
Self::Io(e) => Some(e),
#[cfg(not(feature = "std"))]
Self::Io(e) => Some(e),
Self::Network {
source: Some(s), ..
}
| Self::Config {
source: Some(s), ..
}
| Self::Internal {
source: Some(s), ..
} => Some(s.as_ref()),
Self::Foreign { error, .. } => Some(error.as_ref()),
Self::Multiple {
errors,
primary_index,
} => {
if let Some(idx) = primary_index {
errors.get(*idx).map(|e| e as &dyn Error)
} else {
errors.first().map(|e| e as &dyn Error)
}
}
_ => None,
}
}
}
#[cfg(feature = "std")]
impl Error for YoshiKind {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source()
}
}
#[cfg(not(feature = "std"))]
impl Error for YoshiKind {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source()
}
}
#[derive(Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub struct YoContext {
#[cfg_attr(
feature = "serde",
serde(
serialize_with = "serialize_arc_str",
deserialize_with = "deserialize_arc_str"
)
)]
pub message: Option<Arc<str>>,
#[cfg_attr(
feature = "serde",
serde(
default,
serialize_with = "serialize_arc_str_map",
deserialize_with = "deserialize_arc_str_map"
)
)]
pub metadata: HashMap<Arc<str>, Arc<str>>,
#[cfg_attr(
feature = "serde",
serde(
serialize_with = "serialize_arc_str",
deserialize_with = "deserialize_arc_str"
)
)]
pub suggestion: Option<Arc<str>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub location: Option<YoshiLocation>,
#[cfg_attr(feature = "serde", serde(skip))]
pub payloads: Vec<Arc<Box<dyn Any + Send + Sync + 'static>>>,
pub created_at: Option<SystemTime>,
pub priority: u8,
}
impl YoContext {
#[inline]
pub fn new(msg: impl Into<String>) -> Self {
Self {
message: Some(intern_string(msg.into())),
created_at: Some(SystemTime::now()),
priority: 128, ..Self::default()
}
}
#[must_use]
#[inline]
pub fn with_metadata(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
self.metadata
.insert(intern_string(k.into()), intern_string(v.into()));
self
}
#[must_use]
#[inline]
pub fn with_suggestion(mut self, s: impl Into<String>) -> Self {
self.suggestion = Some(intern_string(s.into()));
self
}
#[must_use]
#[inline]
pub fn with_shell(mut self, shell: impl Any + Send + Sync + 'static) -> Self {
const MAX_PAYLOADS: usize = 16;
if self.payloads.len() < MAX_PAYLOADS {
self.payloads.push(Arc::new(Box::new(shell)));
}
self
}
#[inline]
#[must_use]
pub fn shell<T: 'static>(&self) -> Option<&T> {
self.payloads
.iter()
.find_map(|p_arc| p_arc.as_ref().downcast_ref::<T>())
}
#[inline]
pub fn add_shell_in_place(&mut self, shell: impl Any + Send + Sync + 'static) {
const MAX_PAYLOADS: usize = 16;
if self.payloads.len() < MAX_PAYLOADS {
self.payloads.push(Arc::new(Box::new(shell)));
}
}
#[must_use]
#[inline]
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
#[must_use]
#[inline]
pub fn with_location(mut self, location: YoshiLocation) -> Self {
self.location = Some(location);
self
}
}
impl Clone for YoContext {
fn clone(&self) -> Self {
Self {
message: self.message.clone(),
metadata: self.metadata.clone(),
suggestion: self.suggestion.clone(),
location: self.location,
payloads: self.payloads.clone(),
created_at: self.created_at,
priority: self.priority,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub struct YoshiLocation {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl YoshiLocation {
#[inline]
#[must_use]
pub const fn new(file: &'static str, line: u32, column: u32) -> Self {
Self { file, line, column }
}
#[inline]
#[must_use]
pub fn filename(&self) -> &str {
self.file.rsplit('/').next().unwrap_or(self.file)
}
}
impl Display for YoshiLocation {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}:{}:{}", self.filename(), self.line, self.column)
}
}
#[macro_export]
macro_rules! yoshi_location {
() => {
$crate::YoshiLocation::new(core::file!(), core::line!(), core::column!())
};
}
#[macro_export]
macro_rules! yum {
($err:expr) => {{
let _y: &$crate::Yoshi = &$err;
eprintln!("🍽️ Yoshi consumed error [{}]: {}", _y.instance_id(), _y); if let Some(_laytext) = _y.laytext() {
eprintln!(" 📝 Context: {}", _laytext);
}
if let Some(_suggestion) = _y.suggestion() {
eprintln!(" 💡 Suggestion: {}", _suggestion);
}
if let Some(_nest) = _y.nest() {
eprintln!(" 🥚 Nested: {}", _nest);
}
let analysis = _y.analyze_contexts();
if analysis.total_contexts > 0 {
eprintln!(
" 📊 Analysis: {} contexts, {} metadata entries, severity: {}",
analysis.total_contexts,
analysis.metadata_entries,
_y.severity()
);
}
_y
}};
}
#[derive(Debug)] #[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub struct YoshiBacktrace {
inner: std::backtrace::Backtrace,
capture_timestamp: SystemTime,
thread_id: std::thread::ThreadId,
thread_name: Option<Arc<str>>,
capture_cost_nanos: Option<u64>,
}
#[cfg(feature = "std")]
impl YoshiBacktrace {
#[must_use]
pub fn new_captured() -> Self {
let start = std::time::Instant::now();
let current_thread = thread::current();
let backtrace = std::backtrace::Backtrace::capture();
let capture_cost = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX);
Self {
inner: backtrace,
capture_timestamp: SystemTime::now(),
thread_id: current_thread.id(),
thread_name: current_thread.name().map(std::convert::Into::into),
capture_cost_nanos: Some(capture_cost),
}
}
#[inline]
pub fn status(&self) -> std::backtrace::BacktraceStatus {
self.inner.status()
}
#[inline]
pub fn capture_cost_nanos(&self) -> Option<u64> {
self.capture_cost_nanos
}
}
#[cfg(feature = "std")]
impl Display for YoshiBacktrace {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
const MAX_BACKTRACE_SIZE: usize = 8192;
writeln!(f, "Backtrace captured at: {:?}", self.capture_timestamp)?;
if let Some(ref thread_name) = self.thread_name {
writeln!(f, "Thread: {thread_name} ({:?})", self.thread_id)?;
} else {
writeln!(f, "Thread: {:?}", self.thread_id)?;
}
if let Some(cost) = self.capture_cost_nanos {
writeln!(f, "Capture cost: {cost}ns")?;
}
writeln!(f, "Generated by std::backtrace framework")?;
if is_production_mode() {
write!(f, "[Backtrace sanitized for production]")
} else {
let bt_string = self.inner.to_string();
if bt_string.len() > MAX_BACKTRACE_SIZE {
write!(
f,
"{}... [truncated at {} bytes]",
&bt_string[..MAX_BACKTRACE_SIZE],
MAX_BACKTRACE_SIZE
)
} else {
write!(f, "{bt_string}")
}
}
}
}
#[cfg(not(feature = "std"))]
#[derive(Debug, Clone)]
pub struct YoshiBacktrace {
locations: Vec<YoshiLocation>,
capture_timestamp: SystemTime,
thread_id: ThreadId,
call_depth: u32,
}
#[cfg(not(feature = "std"))]
impl YoshiBacktrace {
pub fn new_captured() -> Self {
Self::new_with_location(yoshi_location!())
}
pub fn new_with_location(location: YoshiLocation) -> Self {
let mut locations = Vec::new();
locations.push(location);
Self {
locations,
capture_timestamp: SystemTime::now(),
thread_id: ThreadId::current(),
call_depth: 1,
}
}
pub fn add_location(&mut self, location: YoshiLocation) {
self.locations.push(location);
self.call_depth += 1;
}
pub const fn call_depth(&self) -> u32 {
self.call_depth
}
pub const fn capture_timestamp(&self) -> SystemTime {
self.capture_timestamp
}
pub const fn thread_id(&self) -> ThreadId {
self.thread_id
}
pub fn locations(&self) -> impl Iterator<Item = &YoshiLocation> {
self.locations.iter()
}
pub fn top_location(&self) -> Option<&YoshiLocation> {
self.locations.last()
}
pub fn status(&self) -> BacktraceStatus {
if self.locations.is_empty() {
BacktraceStatus::Disabled
} else {
BacktraceStatus::Captured
}
}
}
#[cfg(not(feature = "std"))]
impl core::fmt::Display for YoshiBacktrace {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(
f,
"Minimal backtrace (no_std) captured at: {:?}",
self.capture_timestamp
)?;
writeln!(
f,
"Thread: {} | Call depth: {}",
self.thread_id, self.call_depth
)?;
writeln!(f, "Locations:")?;
for (i, location) in self.locations.iter().enumerate() {
writeln!(f, " {}: {}", i, location)?;
}
Ok(())
}
}
#[cfg(not(feature = "std"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BacktraceStatus {
Captured,
Disabled,
Unsupported,
}
pub trait HatchExt<T>
where
Self: Sized,
{
#[track_caller]
fn context(self, msg: impl Into<String>) -> Result<T>;
#[track_caller]
fn with_suggestion(self, s: impl Into<String>) -> Result<T>;
#[track_caller]
fn with_shell(self, p: impl Any + Send + Sync + 'static) -> Result<T>;
#[track_caller]
fn with_priority(self, priority: u8) -> Result<T>;
#[track_caller]
fn ctx(self, msg: impl Into<String>) -> Result<T>;
#[track_caller]
fn help(self, s: impl Into<String>) -> Result<T>;
#[track_caller]
fn meta(self, k: impl Into<String>, v: impl Into<String>) -> Result<T>;
}
#[derive(Debug)]
pub struct Yoshi {
kind: YoshiKind,
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
backtrace: Option<YoshiBacktrace>,
#[cfg(not(feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(not(feature = "std"))))]
backtrace: Option<()>,
contexts: Vec<YoContext>,
instance_id: u32,
#[cfg(feature = "std")]
#[allow(dead_code)]
created_at: SystemTime,
}
impl Clone for Yoshi {
fn clone(&self) -> Self {
let instance_id = ERROR_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
Self {
kind: self.kind.clone(),
#[cfg(feature = "std")]
backtrace: None, #[cfg(not(feature = "std"))]
backtrace: self.backtrace.clone(), contexts: self.contexts.clone(),
instance_id,
#[cfg(feature = "std")]
created_at: SystemTime::now(), }
}
}
impl Yoshi {
#[inline]
pub fn new(kind: YoshiKind) -> Self {
let instance_id = ERROR_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
Self {
kind,
#[cfg(feature = "std")]
backtrace: capture_bt(),
#[cfg(not(feature = "std"))]
backtrace: None,
contexts: Vec::with_capacity(4), instance_id,
#[cfg(feature = "std")]
created_at: SystemTime::now(),
}
}
#[inline]
#[track_caller]
pub fn foreign<E>(e: E) -> Self
where
E: Error + Send + Sync + 'static,
{
Self::new(YoshiKind::from_foreign_with_context(e, ""))
}
#[inline]
pub const fn instance_id(&self) -> u32 {
self.instance_id
}
#[inline]
pub const fn kind(&self) -> &YoshiKind {
&self.kind
}
#[inline]
pub const fn severity(&self) -> u8 {
self.kind.severity()
}
#[inline]
pub const fn is_transient(&self) -> bool {
self.kind.is_transient()
}
#[track_caller]
#[inline]
#[must_use]
pub fn context(mut self, msg: impl Into<String>) -> Self {
self.contexts
.push(YoContext::new(msg).with_location(yoshi_location!()));
self
}
#[inline]
#[track_caller]
#[must_use]
pub fn with_suggestion(mut self, s: impl Into<String>) -> Self {
if self.contexts.is_empty() {
self.contexts
.push(YoContext::new("Error occurred").with_location(yoshi_location!()));
}
self.contexts
.last_mut()
.expect("contexts should not be empty")
.suggestion = Some(intern_string(s.into()));
self
}
#[inline]
#[track_caller]
#[must_use]
pub fn with_component(mut self, component: impl Into<String>) -> Self {
if self.contexts.is_empty() {
self.contexts
.push(YoContext::new("Error occurred").with_location(yoshi_location!()));
}
self.contexts
.last_mut()
.expect("contexts should not be empty")
.metadata
.insert(intern_string("component"), intern_string(component.into()));
self
}
#[inline]
#[track_caller]
#[must_use]
pub fn with_shell(mut self, shell: impl Any + Send + Sync + 'static) -> Self {
if self.contexts.is_empty() {
self.contexts
.push(YoContext::new("Error occurred").with_location(yoshi_location!()));
}
self.contexts
.last_mut()
.expect("contexts should not be empty")
.add_shell_in_place(shell);
self
}
#[inline]
#[must_use]
#[track_caller]
pub fn with_priority(mut self, priority: u8) -> Self {
if self.contexts.is_empty() {
self.contexts
.push(YoContext::new("Error occurred").with_location(yoshi_location!()));
}
self.contexts
.last_mut()
.expect("contexts should not be empty")
.priority = priority;
self
}
#[inline]
#[track_caller]
#[must_use]
pub fn with_metadata(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
if self.contexts.is_empty() {
self.contexts
.push(YoContext::new("Error occurred").with_location(yoshi_location!()));
}
self.contexts
.last_mut()
.expect("contexts should not be empty")
.metadata
.insert(intern_string(k.into()), intern_string(v.into()));
self
}
#[inline]
#[track_caller]
#[must_use]
pub fn with_location(mut self, location: YoshiLocation) -> Self {
if self.contexts.is_empty() {
self.contexts
.push(YoContext::new("Error occurred").with_location(yoshi_location!()));
}
self.contexts
.last_mut()
.expect("contexts should not be empty")
.location = Some(location);
self
}
#[cfg(feature = "std")]
#[inline]
pub const fn backtrace(&self) -> Option<&YoshiBacktrace> {
self.backtrace.as_ref()
}
#[inline]
pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> {
if let YoshiKind::Foreign { error, .. } = &self.kind {
if let Some(result) = error.downcast_ref::<T>() {
return Some(result);
}
if let Some(wrapper) = error.downcast_ref::<ForeignErrorWrapper>() {
wrapper.inner.downcast_ref::<T>()
} else {
None
}
} else {
None
}
}
#[inline]
pub fn downcast_mut<T: Error + 'static>(&mut self) -> Option<&mut T> {
if let YoshiKind::Foreign { error, .. } = &mut self.kind {
if error.is::<ForeignErrorWrapper>() {
if let Some(wrapper) = error.downcast_mut::<ForeignErrorWrapper>() {
wrapper.inner.downcast_mut::<T>()
} else {
None
}
} else {
error.downcast_mut::<T>()
}
} else {
None
}
}
#[inline]
pub fn primary_context(&self) -> Option<&YoContext> {
self.contexts.last()
}
#[inline]
pub fn contexts(&self) -> impl Iterator<Item = &YoContext> {
self.contexts.iter()
}
#[inline]
pub fn suggestion(&self) -> Option<&str> {
self.primary_context()
.and_then(|ctx| ctx.suggestion.as_deref())
}
#[inline]
pub fn shell<T: 'static>(&self) -> Option<&T> {
for context in &self.contexts {
if let Some(shell) = context.shell::<T>() {
return Some(shell);
}
}
None
}
#[inline]
pub fn nest(&self) -> Option<&(dyn Error + 'static)> {
self.kind.source()
}
#[inline]
pub fn laytext(&self) -> Option<&str> {
self.primary_context()
.and_then(|ctx| ctx.message.as_deref())
}
#[track_caller]
#[inline]
#[must_use]
pub fn lay(self, msg: impl Into<String>) -> Self {
self.context(msg)
}
pub fn analyze_contexts(&self) -> ContextAnalysis {
let mut analysis = ContextAnalysis {
total_contexts: self.contexts.len(),
context_depth: self.contexts.len(), ..ContextAnalysis::default()
};
for ctx in &self.contexts {
if ctx.suggestion.is_some() {
analysis.has_suggestions = true;
}
if ctx.location.is_some() {
analysis.has_location_info = true;
}
analysis.metadata_entries += ctx.metadata.len();
analysis.typed_payloads += ctx.payloads.len();
if let Some(primary_ctx) = self.contexts.last() {
analysis.primary_context_priority = primary_ctx.priority;
}
}
analysis
}
}
impl Display for Yoshi {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut buffer = OptimizedFormatBuffer::new();
buffer.append_optimized(&format!("{}: {}", self.instance_id, self.kind));
buffer.append_optimized("\n");
for (i, ctx) in self.contexts.iter().enumerate() {
if i == 0
&& ctx.message.as_deref() == Some("Error occurred")
&& ctx.metadata.is_empty()
&& ctx.suggestion.is_none()
&& ctx.payloads.is_empty()
{
continue;
}
if let Some(msg) = ctx.message.as_deref() {
buffer.append_optimized("Caused by: ");
buffer.append_optimized(msg);
buffer.append_optimized("\n");
}
if !ctx.metadata.is_empty() {
buffer.append_optimized("Metadata:\n");
for (k, v) in &ctx.metadata {
buffer.append_optimized(" ");
buffer.append_optimized(k.as_ref());
buffer.append_optimized(": ");
buffer.append_optimized(v.as_ref());
buffer.append_optimized("\n");
}
}
if let Some(suggestion) = ctx.suggestion.as_deref() {
buffer.append_optimized("Suggestion: ");
buffer.append_optimized(suggestion);
buffer.append_optimized("\n");
}
if let Some(location) = ctx.location {
buffer.append_optimized("Location: ");
buffer.append_optimized(&location.to_string());
buffer.append_optimized("\n");
}
} let mut error_chain: Vec<String> = Vec::new();
let mut yoshi_contexts: Vec<String> = Vec::new();
let mut current_error = self.kind.source();
while let Some(source_error) = current_error {
if let Some(yoshi_source) = source_error.downcast_ref::<Yoshi>() {
error_chain.push(format!("Caused by: {}", yoshi_source.kind));
for ctx in &yoshi_source.contexts {
if let Some(msg) = ctx.message.as_deref() {
yoshi_contexts.push(format!("Caused by: {msg}"));
}
}
current_error = yoshi_source.kind.source();
} else {
error_chain.push(format!("Caused by: {source_error}"));
current_error = source_error.source();
}
}
for error_msg in error_chain {
buffer.append_optimized(&error_msg);
buffer.append_optimized("\n");
}
for ctx_msg in yoshi_contexts {
buffer.append_optimized(&ctx_msg);
buffer.append_optimized("\n");
}
#[cfg(feature = "std")]
if let Some(bt) = &self.backtrace {
buffer.append_optimized("\nBacktrace:\n");
buffer.append_optimized(&bt.to_string());
}
write!(f, "{}", buffer.as_str().trim_end())
}
}
#[cfg(feature = "std")]
impl Error for Yoshi {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.kind.source()
}
}
#[cfg(not(feature = "std"))]
impl Error for Yoshi {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.kind.source()
}
}
#[cfg(docsrs)]
impl Error for Yoshi {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.kind {
YoshiKind::Io(e) => Some(e),
YoshiKind::Foreign { error, .. } => Some(error.as_ref()),
_ => None,
}
}
fn description(&self) -> &str {
"Yoshi error (see Display impl for details)"
}
}
impl From<String> for Yoshi {
#[track_caller]
fn from(s: String) -> Self {
Yoshi::new(YoshiKind::Internal {
message: s.into(),
source: None,
component: None,
})
}
}
impl From<&str> for Yoshi {
#[track_caller]
fn from(s: &str) -> Self {
Yoshi::new(YoshiKind::Internal {
message: s.to_string().into(),
source: None,
component: None,
})
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for Yoshi {
#[track_caller]
fn from(e: std::io::Error) -> Self {
Yoshi::new(YoshiKind::Io(e))
}
}
#[cfg(not(feature = "std"))]
impl From<NoStdIo> for Yoshi {
#[track_caller]
fn from(e: NoStdIo) -> Self {
Yoshi::new(YoshiKind::Io(e))
}
}
impl<T, E> HatchExt<T> for core::result::Result<T, E>
where
E: Into<Yoshi> + Send + Sync + 'static, {
#[track_caller]
#[inline]
fn context(self, msg: impl Into<String>) -> Result<T> {
self.map_err(|e| e.into().context(msg))
}
#[track_caller]
#[inline]
fn with_suggestion(self, s: impl Into<String>) -> Result<T> {
self.map_err(|e| e.into().with_suggestion(s))
}
#[track_caller]
#[inline]
fn with_shell(self, p: impl Any + Send + Sync + 'static) -> Result<T> {
self.map_err(|e| {
let mut yoshi_err = e.into();
if yoshi_err.contexts.is_empty() {
yoshi_err
.contexts
.push(YoContext::default().with_priority(128));
}
yoshi_err.with_shell(p)
})
}
#[track_caller]
#[inline]
fn with_priority(self, priority: u8) -> Result<T> {
self.map_err(|e| e.into().with_priority(priority))
}
#[track_caller]
#[inline]
fn ctx(self, msg: impl Into<String>) -> Result<T> {
self.context(msg)
}
#[track_caller]
#[inline]
fn help(self, s: impl Into<String>) -> Result<T> {
self.with_suggestion(s)
}
#[track_caller]
#[inline]
fn meta(self, k: impl Into<String>, v: impl Into<String>) -> Result<T> {
self.map_err(|e| {
let mut yoshi_err = e.into();
if yoshi_err.contexts.is_empty() {
yoshi_err
.contexts
.push(YoContext::default().with_priority(128));
}
yoshi_err.with_metadata(k, v)
})
}
}
pub trait LayContext<T> {
fn lay(self, message: impl Into<String>) -> Hatch<T>;
}
impl<T> LayContext<T> for Hatch<T> {
#[track_caller]
fn lay(self, message: impl Into<String>) -> Hatch<T> {
self.map_err(|e| e.lay(message))
}
}
pub trait Hatchable<T, E> {
fn hatch(self) -> Hatch<T>;
}
impl<T, E: Into<Yoshi>> Hatchable<T, E> for Result<T, E> {
#[track_caller]
fn hatch(self) -> Hatch<T> {
self.map_err(Into::into)
}
}
#[cfg(feature = "std")]
fn capture_bt() -> Option<YoshiBacktrace> {
let should =
match std::env::var("RUST_LIB_BACKTRACE").or_else(|_| std::env::var("RUST_BACKTRACE")) {
Ok(v) => v == "1" || v == "full", Err(_) => false,
};
if should {
Some(YoshiBacktrace::new_captured())
} else {
None
}
}
pub mod memory {
use super::{error_instance_count, intern_string, Arc, String, STRING_INTERN_POOL};
#[derive(Debug, Default)]
pub struct MemoryStats {
pub total_errors_created: u32,
pub total_contexts_created: u64,
pub string_intern_hits: usize,
pub string_intern_misses: usize,
pub estimated_memory_saved: usize,
}
pub fn get_memory_stats() -> MemoryStats {
let (hits, misses) = STRING_INTERN_POOL
.get()
.map_or((0, 0), super::StringInternPool::stats);
MemoryStats {
total_errors_created: error_instance_count(),
total_contexts_created: 0, string_intern_hits: hits,
string_intern_misses: misses,
estimated_memory_saved: hits * 32, }
}
pub fn efficient_string(s: impl Into<String>) -> Arc<str> {
intern_string(s)
}
#[cfg(feature = "std")]
pub fn cleanup_intern_pool() {
if let Some(pool) = STRING_INTERN_POOL.get() {
pool.clear_pool();
}
}
}
#[cfg(all(feature = "async", not(docsrs)))]
mod async_impl {
#[allow(unused_imports)] pub use crate::async_error_handling::*;
}
#[cfg(all(feature = "async", docsrs))]
mod async_docs {
use crate::{Result, Yoshi};
pub type AsyncResult<T> = core::future::Ready<Result<T, Yoshi>>;
pub fn async_docs_placeholder() -> &'static str {
"Full async functionality available in runtime builds"
}
}
#[cfg(feature = "std")]
pub mod async_error_handling {
use super::{Result, String, Vec, Yoshi, YoshiKind};
use std::future::Future;
use std::time::Duration;
#[cfg(feature = "async")]
#[allow(unused_imports)]
use tokio::time;
pub async fn propagate_async<T, E>(
future: impl Future<Output = Result<T, E>>,
context: impl Into<String>,
) -> Result<T, Yoshi>
where
E: Into<Yoshi>,
{
match future.await {
Ok(value) => Ok(value),
Err(error) => Err(error.into().context(context.into())),
}
}
pub async fn retry_with_backoff<T, F, Fut>(
mut operation: F,
max_retries: usize,
base_delay: Duration,
) -> Result<T, Yoshi>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, Yoshi>>,
{
let mut delay = base_delay;
for attempt in 0..=max_retries {
match operation().await {
Ok(result) => return Ok(result),
Err(error) if attempt == max_retries => return Err(error),
Err(error) if error.is_transient() => {
#[cfg(feature = "async")]
tokio::time::sleep(delay).await;
#[cfg(not(feature = "async"))]
std::thread::sleep(delay);
delay *= 2;
}
Err(error) => return Err(error),
}
}
unreachable!()
}
pub async fn aggregate_errors<I, F, Fut, T>(operations: I) -> Result<Vec<T>, Yoshi>
where
I: IntoIterator<Item = F>,
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T, Yoshi>>,
{
let futures: Vec<_> = operations.into_iter().map(|op| op()).collect();
let mut results = Vec::new();
for fut in futures {
results.push(fut.await);
}
let mut successes = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok(value) => successes.push(value),
Err(error) => errors.push(error),
}
}
if errors.is_empty() {
Ok(successes)
} else {
Err(Yoshi::new(YoshiKind::Multiple {
errors,
primary_index: Some(0),
}))
}
}
}
#[cfg(all(feature = "std", feature = "serde"))]
pub mod process_communication {
use super::{Arc, HashMap, OnceLock, Result, String, SystemTime, ToString, Yoshi};
use serde::{self, Deserializer, Serializer};
use serde_json;
use std::sync::mpsc;
use std::thread;
mod serde_system_time {
use super::{Deserializer, Serializer};
use serde::Deserialize;
use std::time::{SystemTime, UNIX_EPOCH};
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let duration = time
.duration_since(UNIX_EPOCH)
.map_err(serde::ser::Error::custom)?;
serializer.serialize_u64(duration.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
where
D: Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
Ok(UNIX_EPOCH + std::time::Duration::from_secs(secs))
}
}
pub struct ProcessErrorReporter {
sender: mpsc::Sender<ProcessError>,
_handle: thread::JoinHandle<()>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProcessError {
pub process_id: u32,
pub thread_id: String,
pub error_message: String,
pub error_kind: String,
pub severity: u8,
#[serde(with = "serde_system_time")]
pub timestamp: SystemTime,
#[serde(
serialize_with = "super::serde_helpers::serialize_arc_str_map",
deserialize_with = "super::serde_helpers::deserialize_arc_str_map"
)]
pub metadata: HashMap<Arc<str>, Arc<str>>,
}
impl Default for ProcessErrorReporter {
fn default() -> Self {
Self::new()
}
}
impl ProcessErrorReporter {
#[must_use]
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel::<ProcessError>();
let handle = thread::spawn(move || {
while let Ok(error) = receiver.recv() {
eprintln!(
"[PROCESS-ERROR] {}: {} (PID: {}, Severity: {})",
error.timestamp.elapsed().map(|d| d.as_secs()).unwrap_or(0),
error.error_message,
error.process_id,
error.severity
); if let Ok(json_log) = serde_json::to_string(&error) {
println!("STRUCTURED_LOG: {json_log}");
} else {
eprintln!("Failed to serialize process error to JSON.");
}
}
});
Self {
sender,
_handle: handle,
}
}
pub fn report_error(&self, error: &Yoshi) -> Result<(), mpsc::SendError<ProcessError>> {
let process_error = ProcessError {
process_id: std::process::id(),
thread_id: format!("{:?}", std::thread::current().id()),
error_message: error.to_string(),
error_kind: format!("{:?}", error.kind()),
severity: error.severity(),
timestamp: SystemTime::now(),
metadata: error
.primary_context()
.map(|ctx| ctx.metadata.clone())
.unwrap_or_default(),
};
self.sender.send(process_error)
}
}
static PROCESS_REPORTER: OnceLock<ProcessErrorReporter> = OnceLock::new();
pub fn global_reporter() -> &'static ProcessErrorReporter {
PROCESS_REPORTER.get_or_init(ProcessErrorReporter::new)
}
pub fn report_global_error(error: &Yoshi) {
if let Err(e) = global_reporter().report_error(error) {
eprintln!("Failed to report error to cross-process system: {e}");
}
}
}
#[cfg(all(feature = "unstable-metrics", target_arch = "x86_64"))]
pub mod simd_optimization {
use super::{String, ToString, Vec, Yoshi};
pub struct SimdFormatBuffer {
data: Vec<u8>,
capacity: usize,
}
impl SimdFormatBuffer {
#[must_use]
pub fn new() -> Self {
Self::with_capacity(4096)
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
let aligned_capacity = (capacity + 31) & !31;
Self {
data: Vec::with_capacity(aligned_capacity),
capacity: aligned_capacity,
}
}
pub fn append_simd(&mut self, s: &str) {
let bytes = s.as_bytes();
let new_len = self.data.len() + bytes.len();
if new_len > self.capacity {
self.grow_aligned(new_len);
}
if bytes.len() >= 32 {
unsafe { self.append_simd_internal(bytes) };
} else {
self.data.extend_from_slice(bytes);
}
}
#[target_feature(enable = "avx2")]
unsafe fn append_simd_internal(&mut self, bytes: &[u8]) {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{_mm256_loadu_si256, _mm256_storeu_si256};
let chunks = bytes.chunks_exact(32);
let remainder = chunks.remainder();
for chunk in chunks {
let simd_data = _mm256_loadu_si256(chunk.as_ptr().cast());
let dst_ptr = self.data.as_mut_ptr().add(self.data.len()).cast();
_mm256_storeu_si256(dst_ptr, simd_data);
self.data.set_len(self.data.len() + 32);
}
if !remainder.is_empty() {
self.data.extend_from_slice(remainder);
}
}
}
fn grow_aligned(&mut self, min_capacity: usize) {
let new_capacity = ((min_capacity * 2) + 31) & !31;
self.data.reserve_exact(new_capacity - self.data.capacity());
self.capacity = new_capacity;
}
#[must_use]
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(&self.data) }
}
pub fn clear(&mut self) {
self.data.clear();
}
}
impl Default for SimdFormatBuffer {
fn default() -> Self {
Self::new()
}
}
pub fn format_error_simd(error: &Yoshi) -> String {
let mut buffer = SimdFormatBuffer::new();
buffer.append_simd(&format!("{}", error.kind()));
for context in error.contexts() {
if let Some(ref message) = context.message {
buffer.append_simd("\nCaused by: ");
buffer.append_simd(message);
}
}
buffer.as_str().to_string()
}
}
#[cfg(feature = "unstable-metrics")]
pub mod cross_process_metrics {
use super::{OnceLock, SystemTime, Yoshi};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
#[derive(Debug)]
pub struct ErrorMetrics {
total_errors: AtomicU32,
#[allow(dead_code)]
errors_by_kind: HashMap<&'static str, AtomicU32>,
errors_by_severity: [AtomicU32; 256],
memory_usage: AtomicUsize,
#[allow(dead_code)]
processing_time: AtomicU32,
}
impl Default for ErrorMetrics {
fn default() -> Self {
Self {
total_errors: AtomicU32::new(0),
errors_by_kind: HashMap::new(),
errors_by_severity: [const { AtomicU32::new(0) }; 256],
memory_usage: AtomicUsize::new(0),
processing_time: AtomicU32::new(0),
}
}
}
impl ErrorMetrics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_error(&self, error: &Yoshi) {
self.total_errors.fetch_add(1, Ordering::Relaxed);
let severity = error.severity() as usize;
self.errors_by_severity[severity].fetch_add(1, Ordering::Relaxed);
let estimated_size = std::mem::size_of_val(error)
+ error
.contexts()
.map(|ctx| {
ctx.message.as_ref().map_or(0, |m| m.len()) + ctx.metadata.len() * 64
})
.sum::<usize>();
self.memory_usage
.fetch_add(estimated_size, Ordering::Relaxed);
}
#[must_use]
pub fn total_errors(&self) -> u32 {
self.total_errors.load(Ordering::Relaxed)
}
#[must_use]
pub fn errors_by_severity(&self, severity: u8) -> u32 {
self.errors_by_severity[severity as usize].load(Ordering::Relaxed)
}
#[must_use]
pub fn memory_usage(&self) -> usize {
self.memory_usage.load(Ordering::Relaxed)
}
#[must_use]
pub fn generate_report(&self) -> MetricsReport {
MetricsReport {
total_errors: self.total_errors(),
high_severity_errors: (200..=255).map(|s| self.errors_by_severity(s)).sum(),
medium_severity_errors: (100..199).map(|s| self.errors_by_severity(s)).sum(),
low_severity_errors: (0..99).map(|s| self.errors_by_severity(s)).sum(),
memory_usage: self.memory_usage(),
timestamp: SystemTime::now(),
}
}
}
#[derive(Debug, Clone)]
pub struct MetricsReport {
pub total_errors: u32,
pub high_severity_errors: u32,
pub medium_severity_errors: u32,
pub low_severity_errors: u32,
pub memory_usage: usize,
pub timestamp: SystemTime,
}
static GLOBAL_METRICS: OnceLock<ErrorMetrics> = OnceLock::new();
pub fn global_metrics() -> &'static ErrorMetrics {
GLOBAL_METRICS.get_or_init(ErrorMetrics::new)
}
pub fn record_global_error(error: &Yoshi) {
global_metrics().record_error(error);
}
#[must_use]
pub fn global_report() -> MetricsReport {
global_metrics().generate_report()
}
#[cfg(test)]
pub fn reset_global_metrics() {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "std")]
use std::io::ErrorKind;
#[cfg(feature = "std")]
use std::{env, io};
#[test]
fn test_error_instance_counter() {
reset_error_instance_counter();
let initial_count = error_instance_count();
let _err1 = Yoshi::new(YoshiKind::Internal {
message: "test".into(),
source: None,
component: None,
});
let after_first_count = error_instance_count();
assert!(
after_first_count > initial_count,
"Creating first error should increment counter by at least 1"
);
let _err2 = Yoshi::new(YoshiKind::Internal {
message: "test".into(),
source: None,
component: None,
});
let after_second_count = error_instance_count();
assert!(
after_second_count > after_first_count,
"Creating second error should increment counter by at least 1"
);
}
#[test]
fn test_yoshikind_io_display() {
#[cfg(feature = "std")]
{
let io_err = io::Error::new(ErrorKind::NotFound, "file not found");
let kind = YoshiKind::Io(io_err);
assert_eq!(kind.to_string(), "I/O error: file not found");
}
#[cfg(not(feature = "std"))]
{
let kind = YoshiKind::Io(NoStdIo::GenericIo("memory exhausted".into()));
assert_eq!(kind.to_string(), "I/O error (no_std): memory exhausted");
}
}
#[test]
fn test_yoshikind_resource_exhausted_display() {
let kind = YoshiKind::ResourceExhausted {
resource: "memory".into(),
limit: "1GB".into(),
current: "1.2GB".into(),
usage_percentage: Some(120.0),
};
assert_eq!(
kind.to_string(),
"Resource 'memory' exhausted: 1.2GB (limit: 1GB) [120.0% usage]"
);
}
#[test]
fn test_yoshikind_timeout_uses_core_duration() {
let kind = YoshiKind::Timeout {
operation: "long_task".into(),
duration: Duration::from_secs(5),
expected_max: None,
};
assert_eq!(kind.to_string(), "Operation 'long_task' timed out after 5s");
let _duration: Duration = match kind {
YoshiKind::Timeout { duration, .. } => duration,
_ => panic!("Expected Timeout variant"),
};
}
#[test]
fn test_from_std_io_error() {
#[cfg(feature = "std")]
{
let io_err = io::Error::new(ErrorKind::NotFound, "file not found");
let yoshi_err = Yoshi::from(io_err);
assert!(format!("{yoshi_err}").contains("I/O error: file not found"));
assert!(matches!(yoshi_err.kind, YoshiKind::Io(_)));
}
#[cfg(not(feature = "std"))]
{
let no_std_io_err = NoStdIo::new("no_std file not found");
let yoshi_err = Yoshi::from(no_std_io_err);
assert!(format!("{yoshi_err}").contains("I/O error (no_std): not found"));
assert!(matches!(yoshi_err.kind, YoshiKind::Io(_)));
}
}
#[test]
fn test_from_string() {
let msg = "simple string error".to_string();
let yoshi_err = Yoshi::from(msg.clone());
assert!(matches!(
yoshi_err.kind,
YoshiKind::Internal {
ref message, ..
} if message.as_ref() == msg
));
assert!(format!("{yoshi_err}").contains(&msg));
}
#[test]
fn test_yoshi_foreign_from_boxed_error() {
#[derive(Debug)]
struct MyCustomError;
impl Display for MyCustomError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "a custom error occurred")
}
}
impl Error for MyCustomError {}
let boxed_err = Box::new(MyCustomError);
let yoshi_err = Yoshi::foreign(boxed_err); assert!(format!("{yoshi_err}").contains("a custom error occurred"));
assert!(matches!(yoshi_err.kind, YoshiKind::Foreign { .. }));
if let YoshiKind::Foreign {
error_type_name, ..
} = yoshi_err.kind
{
assert_eq!(error_type_name.as_ref(), "alloc::boxed::Box<yoshi_std::tests::test_yoshi_foreign_from_boxed_error::MyCustomError>");
} else {
panic!("Expected Foreign kind");
}
}
#[test]
fn test_contextualization() {
#[cfg(feature = "std")]
let base_err = io::Error::new(ErrorKind::PermissionDenied, "access denied");
#[cfg(not(feature = "std"))]
let base_err = NoStdIo::new("access denied");
let yoshi_err = Yoshi::from(base_err)
.context("Attempted to write to a protected directory".to_string())
.with_metadata("user_id".to_string(), "guest".to_string())
.with_suggestion("Try running with elevated privileges".to_string())
.with_priority(200);
let err_string = format!("{yoshi_err}");
eprintln!("Error string: {err_string}");
#[cfg(feature = "std")]
assert!(err_string.contains("access denied"));
#[cfg(not(feature = "std"))]
assert!(err_string.contains("access denied"));
assert!(err_string.contains("Caused by: Attempted to write to a protected directory"));
assert!(err_string.contains("user_id: guest"));
assert!(err_string.contains("Suggestion: Try running with elevated privileges"));
assert_eq!(yoshi_err.primary_context().unwrap().priority, 200);
}
#[test]
fn test_chained_yoshi_kind() {
let inner_yoshi = Yoshi::new(YoshiKind::Network {
message: "Connection refused".into(),
source: None,
error_code: None,
});
let outer_yoshi = Yoshi::new(YoshiKind::Internal {
message: "Service communication failed".into(),
source: Some(Box::new(inner_yoshi)),
component: None,
});
let err_string = format!("{outer_yoshi}");
assert!(err_string.contains("Internal error: Service communication failed"));
assert!(err_string.contains("Caused by: Network error: Connection refused")); assert!(!err_string.contains("Original Cause: Network error: Connection refused"));
}
#[test]
#[cfg(feature = "std")]
fn test_backtrace_capture_if_enabled() {
let original_rust_backtrace = env::var("RUST_BACKTRACE").ok();
let original_production_mode = env::var("YOSHI_PRODUCTION_MODE").ok();
env::remove_var("YOSHI_PRODUCTION_MODE");
env::set_var("RUST_BACKTRACE", "1");
let err = Yoshi::new(YoshiKind::Internal {
message: "Test internal error with backtrace".into(),
source: None,
component: None,
});
assert!(err.backtrace().is_some());
let formatted_error = format!("{err}");
assert!(formatted_error.contains("Generated by std::backtrace framework"));
assert!(formatted_error.contains("Backtrace captured at:"));
if let Some(val) = original_rust_backtrace {
env::set_var("RUST_BACKTRACE", val);
} else {
env::remove_var("RUST_BACKTRACE");
}
if let Some(val) = original_production_mode {
env::set_var("YOSHI_PRODUCTION_MODE", val);
}
}
#[test]
fn test_no_backtrace_if_disabled() {
#[cfg(feature = "std")]
let original_rust_backtrace = env::var("RUST_BACKTRACE").ok();
#[cfg(feature = "std")]
env::remove_var("RUST_BACKTRACE");
let err = Yoshi::new(YoshiKind::Internal {
message: "No backtrace expected".into(),
source: None,
component: None,
});
#[cfg(feature = "std")]
assert!(err.backtrace().is_none());
#[cfg(not(feature = "std"))]
assert!(err.backtrace.is_none());
assert!(!format!("{err}").contains("stack backtrace"));
#[cfg(feature = "std")]
{
if let Some(val) = original_rust_backtrace {
env::set_var("RUST_BACKTRACE", val);
}
}
}
#[test]
fn test_access_metadata_directly() {
let err = Yoshi::new(YoshiKind::Internal {
message: "Test provide metadata".into(),
source: None,
component: None,
})
.with_metadata("id", "123")
.with_metadata("status", "failed");
let ctx = err
.primary_context()
.expect("Should have a primary context");
let map = &ctx.metadata;
assert_eq!(map.get(&Arc::from("id")), Some(&Arc::from("123")));
assert_eq!(map.get(&Arc::from("status")), Some(&Arc::from("failed")));
}
#[test]
fn test_yoshi_location_macro() {
let loc = yoshi_location!();
assert!(loc.file.ends_with("lib.rs"));
assert!(loc.line > 0);
assert!(loc.column > 0);
assert_eq!(
format!("{loc}"),
format!("{}:{}:{}", loc.filename(), loc.line, loc.column)
);
}
#[test]
fn test_yoshi_with_payload_and_access() {
#[derive(Debug, PartialEq)]
struct CustomErrorPayload {
code: u16,
message: String,
}
impl Display for CustomErrorPayload {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "CustomPayload: code={}, msg={}", self.code, self.message)
}
}
let err = Yoshi::new(YoshiKind::Internal {
message: "Operation failed with custom shell".into(),
source: None,
component: None,
})
.with_shell(CustomErrorPayload {
code: 500,
message: "Internal server error details".into(),
})
.with_shell("a string shell".to_string())
.with_shell(42u32);
let ctx = err
.primary_context()
.expect("Should have a primary context");
let custom_payload = ctx.shell::<CustomErrorPayload>();
assert!(custom_payload.is_some());
assert_eq!(custom_payload.unwrap().code, 500);
let string_payload = ctx.shell::<String>();
assert!(string_payload.is_some());
assert_eq!(string_payload.unwrap(), &"a string shell".to_string());
let u32_payload = ctx.shell::<u32>();
assert!(u32_payload.is_some());
assert_eq!(*u32_payload.unwrap(), 42);
}
#[test]
fn test_yoshi_context_ext_with_payload_on_result() {
#[derive(Debug, PartialEq)]
struct TransactionId(String);
#[cfg(feature = "std")]
let result: std::result::Result<u32, std::io::Error> = Err(io::Error::new(
ErrorKind::PermissionDenied,
"db write failed",
));
#[cfg(not(feature = "std"))]
let result: core::result::Result<u32, NoStdIo> = Err(NoStdIo::new("db write failed"));
let yoshi_result = result
.with_shell(TransactionId("tx123".into()))
.context("Failed to commit transaction".to_string());
assert!(yoshi_result.is_err());
let err = yoshi_result.unwrap_err();
assert!(format!("{err}").contains("db write failed"));
assert!(format!("{err}").contains("Caused by: Failed to commit transaction")); let transaction_id = err.shell::<TransactionId>();
assert!(transaction_id.is_some(), "Should find TransactionId shell");
assert_eq!(transaction_id.unwrap().0, "tx123".to_string());
}
#[test]
fn test_yoshi_context_ext_short_aliases() {
#[cfg(feature = "std")]
let result: std::result::Result<(), std::io::Error> = Err(io::Error::new(
io::ErrorKind::NotFound,
"file.txt not found",
));
#[cfg(not(feature = "std"))]
let result: core::result::Result<(), NoStdIo> = Err(NoStdIo::NotFound);
let err = result
.ctx("Failed to open file".to_string())
.help("Check file path and permissions".to_string())
.meta("file_name".to_string(), "file.txt".to_string())
.unwrap_err();
let s = format!("{err}");
assert!(s.contains("Failed to open file"));
assert!(s.contains("Check file path and permissions"));
assert!(s.contains("file_name: file.txt"));
}
#[test]
fn test_hatch_type_alias() {
let success: Hatch<u32> = Ok(42);
if let Ok(value) = success {
assert_eq!(value, 42);
} else {
panic!("Expected Ok value");
}
let failure: Hatch<u32> = Err(Yoshi::new(YoshiKind::Internal {
message: "test error".into(),
source: None,
component: None,
}));
assert!(failure.is_err());
}
#[test]
fn test_lay_context_trait() {
let error = Yoshi::new(YoshiKind::Internal {
message: "base error".into(),
source: None,
component: None,
});
let result: Hatch<()> = Err(error).lay("additional context");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("additional context"));
}
#[test]
fn test_hatchable_trait() {
#[cfg(feature = "std")]
{
use std::io;
let io_result: Result<String, io::Error> =
Err(io::Error::new(io::ErrorKind::NotFound, "file not found"));
let hatched = io_result.hatch();
assert!(hatched.is_err());
}
let string_result: Result<i32, String> = Err("conversion failed".to_string());
let hatched = string_result.hatch();
assert!(hatched.is_err());
}
#[test]
fn test_yoshi_enhanced_methods() {
let error = Yoshi::new(YoshiKind::Internal {
message: "base error".into(),
source: None,
component: None,
})
.lay("operation context");
assert_eq!(error.laytext().unwrap(), "operation context");
assert!(error.nest().is_none());
}
#[test]
fn test_yum_macro() {
let error = Yoshi::new(YoshiKind::Internal {
message: "test error for yum".into(),
source: None,
component: None,
})
.context("test context")
.with_suggestion("try again");
let returned_error = yum!(error);
assert_eq!(returned_error.laytext().unwrap(), "test context");
assert_eq!(returned_error.suggestion().unwrap(), "try again");
}
#[test]
fn test_hatch_backwards_compatibility() {
use core::error::Error;
let error = Yoshi::new(YoshiKind::Internal {
message: "compatibility test".into(),
source: None,
component: None,
});
let error_ref: &dyn Error = &error;
assert!(error_ref.source().is_none());
assert!(error.nest().is_none()); assert!(error.laytext().is_none()); }
#[test]
fn test_hatch_ecosystem_integration() {
fn complex_operation() -> Hatch<u32> {
#[cfg(feature = "std")]
let io_result: Result<String, std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"access denied",
));
#[cfg(not(feature = "std"))]
let io_result: Result<String, NoStdIo> = Err(NoStdIo::PermissionDenied);
io_result
.hatch()
.lay("while accessing configuration")
.context("during system initialization")
.map_err(|e| {
e.with_metadata("component", "config_loader")
.with_suggestion("check file permissions")
})?;
Ok(42)
}
let result = complex_operation();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.laytext().is_some());
assert_eq!(error.suggestion().unwrap(), "check file permissions");
assert!(error.nest().is_some());
let debug_error = yum!(error);
assert_eq!(debug_error.instance_id(), error.instance_id());
}
}