Skip to main content

masterror/app_error/core/
builder.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2//
3// SPDX-License-Identifier: MIT
4
5use alloc::{borrow::Cow, string::String, sync::Arc};
6use core::error::Error as CoreError;
7#[cfg(feature = "backtrace")]
8use std::backtrace::Backtrace;
9
10#[cfg(feature = "serde_json")]
11use serde::Serialize;
12#[cfg(feature = "serde_json")]
13use serde_json::{Value as JsonValue, to_value};
14
15use super::{
16    error::Error,
17    types::{CapturedBacktrace, ContextAttachment, MessageEditPolicy}
18};
19use crate::{
20    AppCode, AppErrorKind, RetryAdvice,
21    app_error::metadata::{Field, FieldRedaction, Metadata}
22};
23
24impl Error {
25    /// Create a new [`Error`] with a kind and message.
26    ///
27    /// This is equivalent to [`Error::with`], provided for API symmetry and to
28    /// keep doctests readable.
29    ///
30    /// # Examples
31    ///
32    /// ```rust
33    /// use masterror::{AppError, AppErrorKind};
34    /// let err = AppError::new(AppErrorKind::BadRequest, "invalid payload");
35    /// assert!(err.message.is_some());
36    /// ```
37    #[must_use]
38    pub fn new(kind: AppErrorKind, msg: impl Into<Cow<'static, str>>) -> Self {
39        Self::with(kind, msg)
40    }
41
42    /// Create an error with the given kind and message.
43    ///
44    /// Prefer named helpers (e.g. [`Error::not_found`]) where it clarifies
45    /// intent.
46    ///
47    /// # Examples
48    ///
49    /// ```rust
50    /// use masterror::{AppError, AppErrorKind};
51    /// let err = AppError::with(AppErrorKind::Validation, "bad input");
52    /// assert_eq!(err.kind, AppErrorKind::Validation);
53    /// ```
54    #[must_use]
55    pub fn with(kind: AppErrorKind, msg: impl Into<Cow<'static, str>>) -> Self {
56        let err = Self::new_raw(kind, Some(msg.into()));
57        err.emit_telemetry();
58        err
59    }
60
61    /// Create a message-less error with the given kind.
62    ///
63    /// Useful when the kind alone conveys sufficient information to the client.
64    ///
65    /// # Examples
66    ///
67    /// ```rust
68    /// use masterror::{AppError, AppErrorKind};
69    /// let err = AppError::bare(AppErrorKind::NotFound);
70    /// assert!(err.message.is_none());
71    /// ```
72    #[must_use]
73    pub fn bare(kind: AppErrorKind) -> Self {
74        let err = Self::new_raw(kind, None);
75        err.emit_telemetry();
76        err
77    }
78
79    /// Override the machine-readable [`AppCode`].
80    ///
81    /// # Examples
82    ///
83    /// ```rust
84    /// use masterror::{AppCode, AppError, AppErrorKind};
85    /// let err = AppError::new(AppErrorKind::BadRequest, "test").with_code(AppCode::NotFound);
86    /// assert_eq!(err.code, AppCode::NotFound);
87    /// ```
88    #[must_use]
89    pub fn with_code(mut self, code: AppCode) -> Self {
90        self.code = code;
91        self.mark_dirty();
92        self
93    }
94
95    /// Attach retry advice to the error.
96    ///
97    /// When mapped to HTTP, this becomes the `Retry-After` header.
98    ///
99    /// # Examples
100    ///
101    /// ```rust
102    /// use masterror::{AppError, AppErrorKind};
103    /// let err = AppError::new(AppErrorKind::RateLimited, "slow down").with_retry_after_secs(60);
104    /// assert_eq!(err.retry.map(|r| r.after_seconds), Some(60));
105    /// ```
106    #[must_use]
107    pub fn with_retry_after_secs(mut self, secs: u64) -> Self {
108        self.retry = Some(RetryAdvice {
109            after_seconds: secs
110        });
111        self.mark_dirty();
112        self
113    }
114
115    /// Attach a `WWW-Authenticate` challenge string.
116    ///
117    /// # Examples
118    ///
119    /// ```rust
120    /// use masterror::{AppError, AppErrorKind};
121    /// let err = AppError::new(AppErrorKind::Unauthorized, "auth required")
122    ///     .with_www_authenticate("Bearer realm=\"api\"");
123    /// assert!(err.www_authenticate.is_some());
124    /// ```
125    #[must_use]
126    pub fn with_www_authenticate(mut self, value: impl Into<String>) -> Self {
127        self.www_authenticate = Some(value.into());
128        self.mark_dirty();
129        self
130    }
131
132    /// Attach additional metadata to the error.
133    ///
134    /// # Examples
135    ///
136    /// ```rust
137    /// use masterror::{AppError, AppErrorKind, field};
138    /// let err = AppError::new(AppErrorKind::Validation, "bad field")
139    ///     .with_field(field::str("field_name", "email"));
140    /// assert!(err.metadata().get("field_name").is_some());
141    /// ```
142    #[must_use]
143    pub fn with_field(mut self, field: Field) -> Self {
144        self.metadata.insert(field);
145        self.mark_dirty();
146        self
147    }
148
149    /// Extend metadata from an iterator of fields.
150    ///
151    /// # Examples
152    ///
153    /// ```rust
154    /// use masterror::{AppError, AppErrorKind, field};
155    /// let fields = vec![field::str("key1", "value1"), field::str("key2", "value2")];
156    /// let err = AppError::new(AppErrorKind::BadRequest, "test").with_fields(fields);
157    /// assert!(err.metadata().get("key1").is_some());
158    /// ```
159    #[must_use]
160    pub fn with_fields(mut self, fields: impl IntoIterator<Item = Field>) -> Self {
161        self.metadata.extend(fields);
162        self.mark_dirty();
163        self
164    }
165
166    /// Override the redaction policy for a stored metadata field.
167    ///
168    /// # Examples
169    ///
170    /// ```rust
171    /// use masterror::{AppError, AppErrorKind, FieldRedaction, field};
172    ///
173    /// let err = AppError::new(AppErrorKind::Internal, "test")
174    ///     .with_field(field::str("password", "secret"))
175    ///     .redact_field("password", FieldRedaction::Redact);
176    /// ```
177    #[must_use]
178    pub fn redact_field(mut self, name: &'static str, redaction: FieldRedaction) -> Self {
179        self.metadata.set_redaction(name, redaction);
180        self.mark_dirty();
181        self
182    }
183
184    /// Replace metadata entirely.
185    ///
186    /// # Examples
187    ///
188    /// ```rust
189    /// use masterror::{AppError, AppErrorKind, Metadata};
190    ///
191    /// let metadata = Metadata::new();
192    /// let err = AppError::new(AppErrorKind::Internal, "test").with_metadata(metadata);
193    /// ```
194    #[must_use]
195    pub fn with_metadata(mut self, metadata: Metadata) -> Self {
196        self.metadata = metadata;
197        self.mark_dirty();
198        self
199    }
200
201    /// Mark the message as redactable.
202    ///
203    /// # Examples
204    ///
205    /// ```rust
206    /// use masterror::{AppError, AppErrorKind, MessageEditPolicy};
207    ///
208    /// let err = AppError::new(AppErrorKind::Internal, "secret").redactable();
209    /// assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
210    /// ```
211    #[must_use]
212    pub fn redactable(mut self) -> Self {
213        self.edit_policy = MessageEditPolicy::Redact;
214        self.mark_dirty();
215        self
216    }
217
218    /// Attach upstream diagnostics using [`with_source`](Self::with_source) or
219    /// an existing [`Arc`].
220    ///
221    /// This is the preferred alias for capturing upstream errors. It accepts
222    /// either an owned error implementing [`core::error::Error`] or a
223    /// shared [`Arc`] produced by other APIs, reusing the allocation when
224    /// possible.
225    ///
226    /// # Examples
227    ///
228    /// ```rust
229    /// # #[cfg(feature = "std")] {
230    /// use masterror::AppError;
231    ///
232    /// let err = AppError::service("downstream degraded")
233    ///     .with_context(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
234    /// assert!(err.source_ref().is_some());
235    /// # }
236    /// ```
237    #[must_use]
238    pub fn with_context(self, context: impl Into<ContextAttachment>) -> Self {
239        match context.into() {
240            ContextAttachment::Owned(source) => {
241                match source.downcast::<Arc<dyn CoreError + Send + Sync + 'static>>() {
242                    Ok(shared) => self.with_source_arc(*shared),
243                    Err(source) => self.with_source_arc(Arc::from(source))
244                }
245            }
246            ContextAttachment::Shared(source) => self.with_source_arc(source)
247        }
248    }
249
250    /// Attach a source error for diagnostics.
251    ///
252    /// Prefer [`with_context`](Self::with_context) when capturing upstream
253    /// diagnostics without additional `Arc` allocations.
254    ///
255    /// # Examples
256    ///
257    /// ```rust
258    /// # #[cfg(feature = "std")] {
259    /// use masterror::{AppError, AppErrorKind};
260    ///
261    /// let io_err = std::io::Error::new(std::io::ErrorKind::Other, "boom");
262    /// let err = AppError::internal("boom").with_source(io_err);
263    /// assert!(err.source_ref().is_some());
264    /// # }
265    /// ```
266    #[must_use]
267    pub fn with_source(mut self, source: impl CoreError + Send + Sync + 'static) -> Self {
268        self.source = Some(Arc::new(source));
269        self.mark_dirty();
270        self
271    }
272
273    /// Attach a shared source error without cloning the underlying `Arc`.
274    ///
275    /// # Examples
276    ///
277    /// ```rust
278    /// # #[cfg(feature = "std")] {
279    /// use std::sync::Arc;
280    ///
281    /// use masterror::{AppError, AppErrorKind};
282    ///
283    /// let source = Arc::new(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
284    /// let err = AppError::internal("boom").with_source_arc(source.clone());
285    /// assert!(err.source_ref().is_some());
286    /// assert_eq!(Arc::strong_count(&source), 2);
287    /// # }
288    /// ```
289    #[must_use]
290    pub fn with_source_arc(mut self, source: Arc<dyn CoreError + Send + Sync + 'static>) -> Self {
291        self.source = Some(source);
292        self.mark_dirty();
293        self
294    }
295
296    /// Attach a captured backtrace.
297    ///
298    /// # Examples
299    ///
300    /// ```rust
301    /// # #[cfg(feature = "backtrace")]
302    /// # {
303    /// use std::backtrace::Backtrace;
304    ///
305    /// use masterror::AppError;
306    ///
307    /// let bt = Backtrace::capture();
308    /// let err = AppError::internal("test").with_backtrace(bt);
309    /// # }
310    /// ```
311    #[must_use]
312    pub fn with_backtrace(mut self, backtrace: CapturedBacktrace) -> Self {
313        #[cfg(feature = "backtrace")]
314        {
315            self.set_backtrace_slot(Arc::new(backtrace));
316        }
317        #[cfg(not(feature = "backtrace"))]
318        {
319            self.set_backtrace_slot(backtrace);
320        }
321        self.mark_dirty();
322        self
323    }
324
325    /// Attach a shared backtrace without cloning.
326    ///
327    /// Internal method for sharing backtraces between errors.
328    #[cfg(feature = "backtrace")]
329    pub(crate) fn with_shared_backtrace(mut self, backtrace: Arc<Backtrace>) -> Self {
330        self.set_backtrace_slot(backtrace);
331        self.mark_dirty();
332        self
333    }
334
335    /// Attach structured JSON details for the client payload.
336    ///
337    /// The details are omitted from responses when the error has been marked as
338    /// [`redactable`](Self::redactable).
339    ///
340    /// # Examples
341    ///
342    /// ```rust
343    /// # #[cfg(feature = "serde_json")]
344    /// # {
345    /// use masterror::{AppError, AppErrorKind};
346    /// use serde_json::json;
347    ///
348    /// let err = AppError::new(AppErrorKind::Validation, "invalid input")
349    ///     .with_details_json(json!({"field": "email"}));
350    /// assert!(err.details.is_some());
351    /// # }
352    /// ```
353    #[must_use]
354    #[cfg(feature = "serde_json")]
355    pub fn with_details_json(mut self, details: JsonValue) -> Self {
356        self.details = Some(details);
357        self.mark_dirty();
358        self
359    }
360
361    /// Serialize and attach structured details.
362    ///
363    /// Returns [`crate::AppError`] with [`crate::AppErrorKind::BadRequest`] if
364    /// serialization fails.
365    ///
366    /// # Examples
367    ///
368    /// ```rust
369    /// # #[cfg(feature = "serde_json")]
370    /// # {
371    /// use masterror::{AppError, AppErrorKind};
372    /// use serde::Serialize;
373    ///
374    /// #[derive(Serialize)]
375    /// struct Extra {
376    ///     reason: &'static str
377    /// }
378    ///
379    /// let err = AppError::new(AppErrorKind::BadRequest, "invalid")
380    ///     .with_details(Extra {
381    ///         reason: "missing"
382    ///     })
383    ///     .expect("details should serialize");
384    /// assert!(err.details.is_some());
385    /// # }
386    /// ```
387    #[cfg(feature = "serde_json")]
388    #[allow(clippy::result_large_err)]
389    pub fn with_details<T>(self, payload: T) -> crate::AppResult<Self>
390    where
391        T: Serialize
392    {
393        let details = to_value(payload).map_err(|err| Self::bad_request(err.to_string()))?;
394        Ok(self.with_details_json(details))
395    }
396
397    /// Attach plain-text details for client payloads.
398    ///
399    /// The text is omitted from responses when the error is
400    /// [`redactable`](Self::redactable).
401    ///
402    /// # Examples
403    ///
404    /// ```rust
405    /// # #[cfg(not(feature = "serde_json"))]
406    /// # {
407    /// use masterror::{AppError, AppErrorKind};
408    ///
409    /// let err = AppError::new(AppErrorKind::Internal, "boom").with_details_text("retry later");
410    /// assert!(err.details.is_some());
411    /// # }
412    /// ```
413    #[must_use]
414    #[cfg(not(feature = "serde_json"))]
415    pub fn with_details_text(mut self, details: impl Into<String>) -> Self {
416        self.details = Some(details.into());
417        self.mark_dirty();
418        self
419    }
420}