Skip to main content

rustolio_utils/
error.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11use std::panic::Location;
12
13use wasm_bindgen::JsValue;
14
15use crate::prelude::*;
16
17pub type Result<T> = core::result::Result<T, Error>;
18
19pub struct Error {
20    context: String,
21    inner: InnerError,
22
23    #[cfg(debug_assertions)]
24    caller: &'static Location<'static>,
25}
26
27impl Encode for Error {
28    #[async_impl]
29    async fn encode_async(&self, writer: &mut impl __utils_macros::AsyncEncoder) -> Result<()> {
30        self.to_string().encode_async(writer).await?;
31        Ok(())
32    }
33    fn encode_size(&self) -> usize {
34        self.context.encode_size()
35    }
36}
37
38impl Decode for Error {
39    #[async_impl]
40    async fn decode_async(reader: &mut impl __utils_macros::AsyncDecoder) -> Result<Self> {
41        let context = String::decode_async(reader).await?;
42        Ok(Self {
43            context,
44            inner: InnerError::None,
45            #[cfg(debug_assertions)]
46            caller: Location::caller(), // Placeholder
47        })
48    }
49}
50
51impl Error {
52    #[track_caller]
53    pub fn new(context: impl std::fmt::Display) -> Self {
54        Self::new_inner(context, InnerError::None)
55    }
56
57    #[track_caller]
58    fn new_inner(context: impl std::fmt::Display, inner: InnerError) -> Self {
59        Error {
60            context: context.to_string(),
61            inner,
62
63            #[cfg(debug_assertions)]
64            caller: Location::caller(),
65        }
66    }
67
68    pub fn context(&self) -> &str {
69        &self.context
70    }
71}
72
73#[cfg(target_arch = "wasm32")]
74fn strigify_js(js_value: &JsValue) -> String {
75    if let Ok(s) = js_sys::JSON::stringify(js_value) {
76        return String::from(s);
77    }
78    String::from("Failed to stringify")
79}
80
81impl std::fmt::Debug for Error {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        if !self.context.is_empty() {
84            f.write_fmt(format_args!("\"{}\"", &self.context))?;
85        }
86        match (&self.inner, self.context.is_empty()) {
87            (InnerError::None, true) => {
88                f.write_str("Error")?;
89            }
90            (InnerError::None, false) => {}
91            (InnerError::Custom(e), _) => {
92                f.write_str(": ")?;
93                std::fmt::Debug::fmt(e, f)?;
94            }
95            #[cfg(target_arch = "wasm32")]
96            (InnerError::JsValue(j), true) => {
97                f.write_str("JS-Error: ")?;
98                f.write_str(&strigify_js(j))?;
99            }
100            #[cfg(target_arch = "wasm32")]
101            (InnerError::JsValue(j), false) => {
102                f.write_str(": ")?;
103                f.write_str(&strigify_js(j))?;
104            }
105        }
106
107        #[cfg(debug_assertions)]
108        {
109            f.write_str(" at ")?;
110            f.write_fmt(format_args!("{}", self.caller))?;
111        }
112
113        Ok(())
114    }
115}
116
117impl std::fmt::Display for Error {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(
120            f,
121            "{}",
122            match self.context.is_empty() {
123                true => match self.inner {
124                    InnerError::None => "None Value",
125                    InnerError::Custom(_) => "Error",
126                    #[cfg(target_arch = "wasm32")]
127                    InnerError::JsValue(_) => "JS-Error",
128                },
129                false => self.context(),
130            }
131        )
132    }
133}
134
135impl std::error::Error for Error {}
136
137#[cfg(not(target_arch = "wasm32"))]
138trait CustomError: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static {}
139#[cfg(not(target_arch = "wasm32"))]
140impl<T> CustomError for T where T: std::fmt::Debug + Send + Sync + std::fmt::Display + 'static {}
141
142#[cfg(target_arch = "wasm32")]
143trait CustomError: std::fmt::Debug + std::fmt::Display + 'static {}
144#[cfg(target_arch = "wasm32")]
145impl<T> CustomError for T where T: std::fmt::Debug + std::fmt::Display + 'static {}
146
147#[derive(Debug)]
148enum InnerError {
149    None,
150    #[cfg(target_arch = "wasm32")]
151    JsValue(JsValue),
152    Custom(Box<dyn CustomError>),
153}
154
155pub trait FromCustom
156where
157    Self: Sized,
158{
159    #[track_caller]
160    fn with_context(self, context: impl std::fmt::Display) -> Error;
161}
162
163impl<E> FromCustom for E
164where
165    E: CustomError,
166{
167    #[track_caller]
168    fn with_context(self, context: impl std::fmt::Display) -> Error {
169        Error::new_inner(context, InnerError::Custom(Box::new(self)))
170    }
171}
172
173pub trait FromCustomError<T>
174where
175    Self: Sized,
176{
177    #[track_caller]
178    fn context(self, context: impl std::fmt::Display) -> Result<T>;
179}
180
181impl<T, E> FromCustomError<T> for std::result::Result<T, E>
182where
183    E: CustomError,
184{
185    #[track_caller]
186    fn context(self, context: impl std::fmt::Display) -> Result<T> {
187        self.map_err(|e| e.with_context(context))
188    }
189}
190
191impl<T> FromCustomError<T> for Option<T> {
192    #[track_caller]
193    fn context(self, context: impl std::fmt::Display) -> Result<T> {
194        self.ok_or(Error::new_inner(context, InnerError::None))
195    }
196}
197
198pub trait FromJsValue<T>
199where
200    Self: Sized,
201{
202    #[track_caller]
203    fn context(self, context: impl std::fmt::Display) -> Result<T>;
204}
205
206impl<T> FromJsValue<T> for std::result::Result<T, JsValue> {
207    #[track_caller]
208    fn context(self, context: impl std::fmt::Display) -> Result<T> {
209        #[cfg(not(target_arch = "wasm32"))]
210        unreachable!("JsValue cannot be constructed outside wasm32: {}", context);
211
212        #[cfg(target_arch = "wasm32")]
213        self.map_err(|e| Error::new_inner(context, InnerError::JsValue(e)))
214    }
215}
216
217#[cfg(target_arch = "wasm32")]
218impl From<JsValue> for Error {
219    #[track_caller]
220    fn from(value: JsValue) -> Self {
221        Error::new_inner("Unreconverable JS-Error", InnerError::JsValue(value))
222    }
223}
224
225#[cfg(not(target_arch = "wasm32"))]
226impl From<JsValue> for Error {
227    fn from(_: JsValue) -> Self {
228        unreachable!("JsValue cannot be constructed outside wasm32");
229    }
230}