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