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}