Skip to main content

ntex_error/
utils.rs

1use std::{
2    borrow::Cow, cell::RefCell, convert::Infallible, error::Error as StdError, fmt, io,
3    path::Path, path::PathBuf,
4};
5
6use ntex_bytes::ByteString;
7
8use crate::{Error, ErrorDiagnostic, ResultType};
9
10/// The retry policy of the error.
11pub trait Retryable {
12    fn is_retryable(&self) -> bool;
13}
14
15impl<T, E> Retryable for Result<T, E>
16where
17    E: Retryable,
18{
19    fn is_retryable(&self) -> bool {
20        match self {
21            Ok(_) => false,
22            Err(err) => err.is_retryable(),
23        }
24    }
25}
26
27/// Helper type holding a result type and classification signature.
28#[derive(Copy, Clone, Debug, PartialEq, Eq)]
29pub struct ResultSignature(pub &'static str);
30
31impl ResultSignature {
32    /// Construct new `ResultInfo`
33    pub fn new(sig: &'static str) -> Self {
34        Self(sig)
35    }
36
37    /// Returns a stable identifier for the result classification.
38    pub fn signature(self) -> &'static str {
39        self.0
40    }
41}
42
43impl<'a, E: ErrorDiagnostic> From<&'a E> for ResultSignature {
44    fn from(err: &'a E) -> Self {
45        ResultSignature::new(err.signature())
46    }
47}
48
49impl<'a, T, E: ErrorDiagnostic> From<&'a Result<T, E>> for ResultSignature {
50    fn from(result: &'a Result<T, E>) -> Self {
51        match result {
52            Ok(_) => ResultSignature(ResultType::Success.as_str()),
53            Err(err) => ResultSignature(err.signature()),
54        }
55    }
56}
57
58impl ErrorDiagnostic for Infallible {
59    fn signature(&self) -> &'static str {
60        unreachable!()
61    }
62}
63
64impl ErrorDiagnostic for io::Error {
65    fn typ(&self) -> ResultType {
66        match self.kind() {
67            io::ErrorKind::InvalidData
68            | io::ErrorKind::InvalidInput
69            | io::ErrorKind::Unsupported
70            | io::ErrorKind::UnexpectedEof
71            | io::ErrorKind::BrokenPipe
72            | io::ErrorKind::ConnectionReset
73            | io::ErrorKind::ConnectionAborted
74            | io::ErrorKind::NotConnected
75            | io::ErrorKind::TimedOut => ResultType::ClientError,
76            _ => ResultType::ServiceError,
77        }
78    }
79
80    fn signature(&self) -> &'static str {
81        match self.kind() {
82            io::ErrorKind::InvalidData => "io-InvalidData",
83            io::ErrorKind::InvalidInput => "io-InvalidInput",
84            io::ErrorKind::Unsupported => "io-Unsupported",
85            io::ErrorKind::UnexpectedEof => "io-UnexpectedEof",
86            io::ErrorKind::BrokenPipe => "io-BrokenPipe",
87            io::ErrorKind::ConnectionReset => "io-ConnectionReset",
88            io::ErrorKind::ConnectionAborted => "io-ConnectionAborted",
89            io::ErrorKind::NotConnected => "io-NotConnected",
90            io::ErrorKind::TimedOut => "io-TimedOut",
91            _ => "io-Error",
92        }
93    }
94}
95
96#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
97pub struct Success;
98
99impl StdError for Success {}
100
101impl ErrorDiagnostic for Success {
102    fn typ(&self) -> ResultType {
103        ResultType::Success
104    }
105
106    fn signature(&self) -> &'static str {
107        ResultType::Success.as_str()
108    }
109}
110
111impl fmt::Display for Success {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "Success")
114    }
115}
116
117/// Executes a future and ensures an error service is set.
118///
119/// If the error does not already have a service, the provided service is assigned.
120pub async fn with_service<F, T, E>(svc: &'static str, fut: F) -> F::Output
121where
122    F: Future<Output = Result<T, Error<E>>>,
123    E: ErrorDiagnostic + Clone,
124{
125    fut.await.map_err(|err: Error<E>| {
126        if err.service().is_none() {
127            err.set_service(svc)
128        } else {
129            err
130        }
131    })
132}
133
134/// Generates a module path from the given file path.
135pub fn module_path(file_path: &str) -> ByteString {
136    module_path_ext("", "", "::", "", file_path)
137}
138
139/// Generates a module path with a prefix from the given file path.
140pub fn module_path_prefix(prefix: &'static str, file_path: &str) -> ByteString {
141    module_path_ext(prefix, "", "::", "", file_path)
142}
143
144/// Generates a module path from a file path.
145pub fn module_path_fs(file_path: &str) -> ByteString {
146    module_path_ext("", "/src", "/", ".rs", file_path)
147}
148
149fn module_path_ext(
150    prefix: &'static str,
151    mod_sep: &str,
152    sep: &str,
153    suffix: &str,
154    file_path: &str,
155) -> ByteString {
156    type HashMap<K, V> = std::collections::HashMap<K, V, foldhash::fast::RandomState>;
157    thread_local! {
158        static CACHE: RefCell<HashMap<&'static str, HashMap<String, ByteString>>> = RefCell::new(HashMap::default());
159    }
160
161    let cached = CACHE.with(|cache| {
162        if let Some(c) = cache.borrow().get(prefix) {
163            c.get(file_path).cloned()
164        } else {
165            None
166        }
167    });
168
169    if let Some(cached) = cached {
170        cached
171    } else {
172        let normalized_file_path = normalize_file_path(file_path);
173        let (module_name, module_root) =
174            module_root_from_file(mod_sep, &normalized_file_path);
175        let module = module_path_from_file_with_root(
176            prefix,
177            sep,
178            &normalized_file_path,
179            &module_name,
180            &module_root,
181            suffix,
182        );
183
184        let _ = CACHE.with(|cache| {
185            cache
186                .borrow_mut()
187                .entry(prefix)
188                .or_default()
189                .insert(file_path.to_string(), module.clone())
190        });
191        module
192    }
193}
194
195fn normalize_file_path(file_path: &str) -> String {
196    let path = Path::new(file_path);
197    if path.is_absolute() {
198        return path.to_string_lossy().into_owned();
199    }
200
201    match std::env::current_dir() {
202        Ok(cwd) => cwd.join(path).to_string_lossy().into_owned(),
203        Err(_) => file_path.to_string(),
204    }
205}
206
207fn module_root_from_file(mod_sep: &str, file_path: &str) -> (String, PathBuf) {
208    let normalized = file_path.replace('\\', "/");
209    if let Some((root, _)) = normalized.rsplit_once("/src/") {
210        let mut root = PathBuf::from(root);
211        let mod_name = root
212            .file_name()
213            .map_or(Cow::Borrowed("crate"), |s| s.to_string_lossy());
214        let mod_name = if mod_sep.is_empty() {
215            mod_name.replace('-', "_")
216        } else {
217            mod_name.to_string()
218        };
219        root.push("src");
220        return (format!("{mod_name}{mod_sep}"), root);
221    }
222
223    let path = Path::new(file_path)
224        .parent()
225        .map_or_else(|| PathBuf::from("."), Path::to_path_buf);
226
227    let m = path
228        .parent()
229        .and_then(|p| p.file_name())
230        .map_or_else(|| Cow::Borrowed("crate"), |p| p.to_string_lossy());
231
232    (format!("{m}{mod_sep}"), path)
233}
234
235fn module_path_from_file(sep: &str, file_path: &str) -> String {
236    let normalized = file_path.replace('\\', "/");
237    let relative = normalized
238        .split_once("/src/")
239        .map_or(normalized.as_str(), |(_, tail)| tail);
240
241    if relative == "lib.rs" || relative == "main.rs" {
242        return relative.to_string();
243    }
244
245    let without_ext = relative.strip_suffix(".rs").unwrap_or(relative);
246    if without_ext.ends_with("/mod") {
247        let parent = without_ext.strip_suffix("/mod").unwrap_or(without_ext);
248        let parent = parent.trim_matches('/');
249        return parent.replace('/', sep);
250    }
251
252    let module = without_ext.trim_matches('/').replace('/', sep);
253    if module.is_empty() {
254        "crate".to_string()
255    } else {
256        module
257    }
258}
259
260fn module_path_from_file_with_root(
261    prefix: &str,
262    sep: &str,
263    file_path: &str,
264    module_name: &str,
265    module_root: &Path,
266    suffix: &str,
267) -> ByteString {
268    let normalized = file_path.replace('\\', "/");
269    let module_root_norm = module_root.to_string_lossy().replace('\\', "/");
270
271    let Some(relative) = normalized.strip_prefix(&(module_root_norm.clone() + "/")) else {
272        return format!(
273            "{prefix}{module_name}{sep}{}{suffix}",
274            module_path_from_file(sep, file_path)
275        )
276        .into();
277    };
278    if relative == "lib.rs" || relative == "main.rs" {
279        return ByteString::from(format!("{prefix}{module_name}{sep}{relative}"));
280    }
281
282    let without_ext = relative.strip_suffix(".rs").unwrap_or(relative);
283    if without_ext.ends_with("/mod") {
284        let parent = without_ext.strip_suffix("/mod").unwrap_or(without_ext);
285        let parent = parent.trim_matches('/');
286        return format!(
287            "{prefix}{module_name}{sep}{}{sep}mod{suffix}",
288            parent.replace('/', sep)
289        )
290        .into();
291    }
292
293    let module = without_ext.trim_matches('/').replace('/', sep);
294    if module.is_empty() {
295        ByteString::from(format!("{prefix}{module_name}{suffix}"))
296    } else {
297        format!("{prefix}{module_name}{sep}{module}{suffix}").into()
298    }
299}