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