hapi_rs/
errors.rs

1use crate::session::Session;
2
3pub use crate::ffi::raw::{HapiResult, StatusType, StatusVerbosity};
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, HapiError>;
7
8/// Error type returned by all APIs
9#[derive(Error, Debug)]
10#[non_exhaustive]
11pub enum HapiError {
12    /// HAPI function call failed
13    Hapi {
14        result_code: HapiResultCode,
15        server_message: Option<String>,
16        contexts: Vec<String>,
17    },
18
19    /// This is used by [`ErrorContext::context`] / [`ErrorContext::with_context`]
20    Context {
21        contexts: Vec<String>,
22        #[source]
23        source: Box<HapiError>,
24    },
25
26    /// CString conversion error - string contains null byte
27    NullByte(#[from] std::ffi::NulError),
28
29    /// UTF-8 conversion error
30    Utf8(#[from] std::string::FromUtf8Error),
31
32    /// IO error
33    Io(#[from] std::io::Error),
34
35    /// Internal library error
36    Internal(String),
37}
38
39// Wrapper for HapiResult to provide Display for error messages
40#[derive(Debug, Clone, Copy)]
41pub struct HapiResultCode(pub HapiResult);
42
43impl std::fmt::Display for HapiResultCode {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        use HapiResult::*;
46        let desc = match self.0 {
47            Success => "SUCCESS",
48            Failure => "FAILURE",
49            AlreadyInitialized => "ALREADY_INITIALIZED",
50            NotInitialized => "NOT_INITIALIZED",
51            CantLoadfile => "CANT_LOADFILE",
52            ParmSetFailed => "PARM_SET_FAILED",
53            InvalidArgument => "INVALID_ARGUMENT",
54            CantLoadGeo => "CANT_LOAD_GEO",
55            CantGeneratePreset => "CANT_GENERATE_PRESET",
56            CantLoadPreset => "CANT_LOAD_PRESET",
57            AssetDefAlreadyLoaded => "ASSET_DEF_ALREADY_LOADED",
58            NoLicenseFound => "NO_LICENSE_FOUND",
59            DisallowedNcLicenseFound => "DISALLOWED_NC_LICENSE_FOUND",
60            DisallowedNcAssetWithCLicense => "DISALLOWED_NC_ASSET_WITH_C_LICENSE",
61            DisallowedNcAssetWithLcLicense => "DISALLOWED_NC_ASSET_WITH_LC_LICENSE",
62            DisallowedLcAssetWithCLicense => "DISALLOWED_LC_ASSET_WITH_C_LICENSE",
63            DisallowedHengineindieW3partyPlugin => "DISALLOWED_HENGINEINDIE_W_3PARTY_PLUGIN",
64            AssetInvalid => "ASSET_INVALID",
65            NodeInvalid => "NODE_INVALID",
66            UserInterrupted => "USER_INTERRUPTED",
67            InvalidSession => "INVALID_SESSION",
68            SharedMemoryBufferOverflow => "SHARED_MEMORY_BUFFER_OVERFLOW",
69            InvalidSharedMemoryBuffer => "INVALID_SHARED_MEMORY_BUFFER",
70        };
71        write!(f, "{}", desc)
72    }
73}
74
75// This special case for TryFrom<T, Error = HapiError> where conversion can't fail.
76// for example when "impl TryInto<AttributeName>" receives AttributeName.
77impl From<std::convert::Infallible> for HapiError {
78    fn from(_: std::convert::Infallible) -> Self {
79        unreachable!()
80    }
81}
82
83impl From<HapiResult> for HapiError {
84    fn from(r: HapiResult) -> Self {
85        HapiError::Hapi {
86            result_code: HapiResultCode(r),
87            server_message: None,
88            contexts: Vec::new(),
89        }
90    }
91}
92
93impl From<&str> for HapiError {
94    fn from(value: &str) -> Self {
95        HapiError::Internal(value.to_string())
96    }
97}
98
99pub(crate) trait ErrorContext<T> {
100    fn context<C>(self, context: C) -> Result<T>
101    where
102        C: Into<String>;
103
104    #[allow(unused)]
105    fn with_context<C, F>(self, func: F) -> Result<T>
106    where
107        C: Into<String>,
108        F: FnOnce() -> C;
109}
110
111impl<T> ErrorContext<T> for Result<T> {
112    fn context<C>(self, context: C) -> Result<T>
113    where
114        C: Into<String>,
115    {
116        match self {
117            Ok(ok) => Ok(ok),
118            Err(mut error) => {
119                let context = context.into();
120                match &mut error {
121                    HapiError::Hapi { contexts, .. } => {
122                        contexts.push(context);
123                        Err(error)
124                    }
125                    HapiError::Context { contexts, .. } => {
126                        contexts.push(context);
127                        Err(error)
128                    }
129                    _ => Err(HapiError::Context {
130                        contexts: vec![context],
131                        source: Box::new(error),
132                    }),
133                }
134            }
135        }
136    }
137
138    fn with_context<C, F>(self, func: F) -> Result<T>
139    where
140        C: Into<String>,
141        F: FnOnce() -> C,
142    {
143        match self {
144            Ok(ok) => Ok(ok),
145            Err(mut error) => {
146                let context = func().into();
147                match &mut error {
148                    HapiError::Hapi { contexts, .. } => {
149                        contexts.push(context);
150                        Err(error)
151                    }
152                    HapiError::Context { contexts, .. } => {
153                        contexts.push(context);
154                        Err(error)
155                    }
156                    _ => Err(HapiError::Context {
157                        contexts: vec![context],
158                        source: Box::new(error),
159                    }),
160                }
161            }
162        }
163    }
164}
165
166// Custom Display to show contexts properly for HAPI errors
167impl std::fmt::Display for HapiError {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        fn fmt_base(err: &HapiError, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170            match err {
171                HapiError::Hapi {
172                    result_code,
173                    server_message,
174                    ..
175                } => {
176                    write!(f, "[{}]", result_code)?;
177                    if let Some(msg) = server_message {
178                        write!(f, ": [Engine Message]: {}", msg)?;
179                    }
180                    Ok(())
181                }
182                HapiError::Context { source, .. } => fmt_base(source, f),
183                HapiError::NullByte(e) => {
184                    let vec = e.clone().into_vec();
185                    let text = String::from_utf8_lossy(&vec);
186                    write!(f, "String contains null byte in \"{}\"", text)
187                }
188                HapiError::Utf8(e) => {
189                    let text = String::from_utf8_lossy(e.as_bytes());
190                    write!(f, "Invalid UTF-8 in string \"{}\"", text)
191                }
192                HapiError::Io(e) => write!(f, "IO error: {}", e),
193                HapiError::Internal(e) => write!(f, "Internal error: {}", e),
194            }
195        }
196
197        fn collect_contexts<'a>(err: &'a HapiError, out: &mut Vec<&'a str>) {
198            match err {
199                HapiError::Hapi { contexts, .. } => {
200                    out.extend(contexts.iter().map(|s| s.as_str()));
201                }
202                HapiError::Context { contexts, source } => {
203                    collect_contexts(source, out);
204                    out.extend(contexts.iter().map(|s| s.as_str()));
205                }
206                _ => {}
207            }
208        }
209
210        fmt_base(self, f)?;
211
212        let mut contexts = Vec::new();
213        collect_contexts(self, &mut contexts);
214        if !contexts.is_empty() {
215            writeln!(f)?;
216            for (n, msg) in contexts.iter().enumerate() {
217                writeln!(f, "\t{}. {}", n, msg)?;
218            }
219        }
220        Ok(())
221    }
222}
223
224impl HapiResult {
225    /// Check HAPI_Result status and convert to HapiError if the status is not success.
226    pub(crate) fn check_err<F, M>(self, session: &Session, context: F) -> Result<()>
227    where
228        M: Into<String>,
229        F: FnOnce() -> M,
230    {
231        match self {
232            HapiResult::Success => Ok(()),
233            _err => {
234                let server_message = if session.is_valid() {
235                    session
236                        .get_status_string(StatusType::CallResult, StatusVerbosity::All)
237                        .ok()
238                } else {
239                    Some("Session is corrupted: error message not available".to_string())
240                };
241
242                Err(HapiError::Hapi {
243                    result_code: HapiResultCode(self),
244                    server_message: server_message
245                        .or_else(|| Some("Could not retrieve error message".to_string())),
246                    contexts: vec![context().into()],
247                })
248            }
249        }
250    }
251
252    /// Convert HAPI_Result to HapiError if the status is not success and add a message to the error.
253    pub(crate) fn add_context<I: Into<String>>(self, message: I) -> Result<()> {
254        match self {
255            HapiResult::Success => Ok(()),
256            _err => Err(HapiError::Hapi {
257                result_code: HapiResultCode(self),
258                server_message: None,
259                contexts: vec![message.into()],
260            }),
261        }
262    }
263
264    pub(crate) fn with_context<F, M>(self, func: F) -> Result<()>
265    where
266        F: FnOnce() -> M,
267        M: Into<String>,
268    {
269        self.add_context(func())
270    }
271
272    pub(crate) fn with_server_message<F, M>(self, func: F) -> Result<()>
273    where
274        F: FnOnce() -> M,
275        M: Into<String>,
276    {
277        match self {
278            HapiResult::Success => Ok(()),
279            _err => Err(HapiError::Hapi {
280                result_code: HapiResultCode(self),
281                server_message: Some(func().into()),
282                contexts: vec![],
283            }),
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use std::error::Error as _;
292
293    #[test]
294    fn context_chain_is_rendered_for_internal_errors() {
295        let err = (Err::<(), HapiError>(HapiError::Internal("root".to_string())))
296            .context("first context")
297            .context("second context")
298            .unwrap_err();
299
300        let s = err.to_string();
301        assert!(s.starts_with("Internal error: root"));
302        assert!(s.contains("\n\t0. first context\n\t1. second context\n"));
303
304        // Verify the source chain is preserved via #[source]
305        let source = err.source().expect("source");
306        assert_eq!(source.to_string(), "Internal error: root");
307    }
308
309    #[test]
310    fn hapi_errors_server_message_and_contexts() {
311        let err = (Err::<(), HapiError>(HapiError::Hapi {
312            result_code: HapiResultCode(HapiResult::Failure),
313            server_message: Some("could not cook".to_string()),
314            contexts: vec!["low-level".to_string()],
315        }))
316        .context("high-level")
317        .unwrap_err();
318
319        let s = err.to_string();
320        assert_eq!(
321            s,
322            "[FAILURE]: [Engine Message]: could not cook\n\t0. low-level\n\t1. high-level\n"
323        );
324    }
325
326    #[test]
327    fn context_added_outside_hapi_error_is_rendered_after_inner_contexts() {
328        // Create a HAPI error with one context, then add an outer wrapper context.
329        let base = HapiError::Hapi {
330            result_code: HapiResultCode(HapiResult::InvalidArgument),
331            server_message: None,
332            contexts: vec!["inner".to_string()],
333        };
334        let wrapped = (Err::<(), HapiError>(base)).context("outer").unwrap_err();
335
336        let s = wrapped.to_string();
337        // Base header comes from the underlying Hapi error
338        assert!(s.starts_with("[INVALID_ARGUMENT]"));
339        // Context order: inner first, then outer
340        assert!(s.contains("\n\t0. inner\n\t1. outer\n"));
341    }
342}