Skip to main content

ntex_error/
utils.rs

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