masterror/
convert.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2//
3// SPDX-License-Identifier: MIT
4
5//! Error conversions (`From<...> for AppError`) and transport-specific
6//! adapters.
7//!
8//! This module collects **conservative mappings** from external errors into
9//! the crate’s [`AppError`]. It also conditionally enables transport adapters
10//! (Axum/Actix) to turn [`AppError`] into HTTP responses.  
11//!
12//! ## Always-on mappings
13//!
14//! - [`std::io::Error`] → `AppErrorKind::Internal`   Infrastructure-level
15//!   failure; message preserved for logs only.
16//! - [`String`] → `AppErrorKind::BadRequest`   Lightweight validation helper
17//!   when you don’t pull in `validator`.
18//!
19//! ## Feature-gated mappings
20//!
21//! Each of these is compiled only when the feature is enabled. They live in
22//! submodules under `convert/`:
23//!
24//! - `axum` + `multipart`: Axum multipart parsing errors
25//! - `actix`: Actix `ResponseError` integration (not a mapping, but transport)
26//! - `config`: configuration loader errors
27//! - `redis`: Redis client errors
28//! - `reqwest`: HTTP client errors
29//! - `serde_json`: JSON conversion helpers (if you attach JSON details
30//!   manually)
31//! - `sqlx`: database driver errors
32//! - `tokio`: timeouts from `tokio::time::error::Elapsed`
33//! - `teloxide`: Telegram request errors
34//! - `validator`: input DTO validation errors
35//!
36//! ## Design notes
37//!
38//! - Mappings prefer **stable, public-facing categories** (`AppErrorKind`).
39//! - **No panics, no unwraps**: all failures become [`AppError`].
40//! - **Logging is not performed here**. The only place errors are logged is at
41//!   the HTTP boundary (e.g. in `IntoResponse` or `ResponseError` impls).
42//! - Transport adapters (`axum`, `actix`) are technically not “conversions”,
43//!   but are colocated here for discoverability. They never leak internal error
44//!   sources; only safe wire payloads are exposed.
45//!
46//! ## Examples
47//!
48//! `std::io::Error` mapping:
49//!
50//! ```rust
51//! # #[cfg(feature = "std")]
52//! # {
53//! use std::io::{self, ErrorKind};
54//!
55//! use masterror::{AppError, AppErrorKind, AppResult};
56//!
57//! fn open() -> AppResult<()> {
58//!     let _ = io::Error::new(ErrorKind::Other, "boom");
59//!     Err(io::Error::from(ErrorKind::Other).into())
60//! }
61//!
62//! let err = open().unwrap_err();
63//! assert!(matches!(err.kind, AppErrorKind::Internal));
64//! # }
65//! ```
66//!
67//! `String` mapping (useful for ad-hoc validation without the `validator`
68//! feature):
69//!
70//! ```rust
71//! use masterror::{AppError, AppErrorKind, AppResult};
72//!
73//! fn validate(x: i32) -> AppResult<()> {
74//!     if x < 0 {
75//!         return Err(String::from("must be non-negative").into());
76//!     }
77//!     Ok(())
78//! }
79//!
80//! let err = validate(-1).unwrap_err();
81//! assert!(matches!(err.kind, AppErrorKind::BadRequest));
82//! ```
83
84use alloc::string::String;
85#[cfg(feature = "std")]
86use std::io::Error as IoError;
87
88use crate::AppError;
89
90#[cfg(feature = "axum")]
91#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
92mod axum;
93
94#[cfg(all(feature = "axum", feature = "multipart"))]
95#[cfg_attr(docsrs, doc(cfg(all(feature = "axum", feature = "multipart"))))]
96mod multipart;
97
98#[cfg(feature = "actix")]
99#[cfg_attr(docsrs, doc(cfg(feature = "actix")))]
100mod actix;
101
102#[cfg(feature = "config")]
103#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
104mod config;
105
106#[cfg(feature = "redis")]
107#[cfg_attr(docsrs, doc(cfg(feature = "redis")))]
108mod redis;
109
110#[cfg(feature = "reqwest")]
111#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
112mod reqwest;
113
114#[cfg(feature = "serde_json")]
115#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
116mod serde_json;
117
118#[cfg(feature = "sqlx")]
119#[cfg_attr(docsrs, doc(cfg(feature = "sqlx")))]
120mod sqlx;
121
122#[cfg(feature = "tokio")]
123#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
124mod tokio;
125
126#[cfg(feature = "validator")]
127#[cfg_attr(docsrs, doc(cfg(feature = "validator")))]
128mod validator;
129
130#[cfg(feature = "teloxide")]
131#[cfg_attr(docsrs, doc(cfg(feature = "teloxide")))]
132mod teloxide;
133
134#[cfg(feature = "init-data")]
135#[cfg_attr(docsrs, doc(cfg(feature = "init-data")))]
136mod init_data;
137
138#[cfg(feature = "tonic")]
139#[cfg_attr(docsrs, doc(cfg(feature = "tonic")))]
140mod tonic;
141
142#[cfg(feature = "tonic")]
143pub use self::tonic::StatusConversionError;
144
145/// Map `std::io::Error` to an internal application error.
146///
147/// Rationale: I/O failures are infrastructure-level and should not leak
148/// driver-specific details to clients. The message is preserved for
149/// observability, but the public-facing kind is always `Internal`.
150///
151/// ```rust
152/// use std::io::{self, ErrorKind};
153///
154/// use masterror::{AppError, AppErrorKind};
155///
156/// let io_err = io::Error::from(ErrorKind::Other);
157/// let app_err: AppError = io_err.into();
158/// assert!(matches!(app_err.kind, AppErrorKind::Internal));
159/// ```
160#[cfg(feature = "std")]
161impl From<IoError> for AppError {
162    fn from(err: IoError) -> Self {
163        AppError::internal(err.to_string())
164    }
165}
166
167/// Map a plain `String` to a client error (`BadRequest`).
168///
169/// Handy for quick validation paths without the `validator` feature.
170/// Prefer structured validation for complex DTOs, but this covers simple cases.
171///
172/// ```rust
173/// use masterror::{AppError, AppErrorKind, AppResult};
174///
175/// fn check(name: &str) -> AppResult<()> {
176///     if name.is_empty() {
177///         return Err(String::from("name must not be empty").into());
178///     }
179///     Ok(())
180/// }
181///
182/// let err = check("").unwrap_err();
183/// assert!(matches!(err.kind, AppErrorKind::BadRequest));
184/// ```
185impl From<String> for AppError {
186    fn from(value: String) -> Self {
187        AppError::bad_request(value)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use crate::{AppError, AppErrorKind};
194
195    // --- std::io::Error -> AppError -----------------------------------------
196
197    #[test]
198    fn io_error_maps_to_internal_and_preserves_message() {
199        use std::io::Error;
200        let src = Error::other("disk said nope");
201        let app: AppError = src.into();
202        assert!(matches!(app.kind, AppErrorKind::Internal));
203        assert_eq!(app.message.as_deref(), Some("disk said nope"));
204    }
205
206    // --- String -> AppError --------------------------------------------------
207
208    #[test]
209    fn string_maps_to_bad_request_and_preserves_text() {
210        let app: AppError = String::from("name must not be empty").into();
211        assert!(matches!(app.kind, AppErrorKind::BadRequest));
212        assert_eq!(app.message.as_deref(), Some("name must not be empty"));
213    }
214}