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#[derive(Error, Debug)]
10#[non_exhaustive]
11pub enum HapiError {
12 Hapi {
14 result_code: HapiResultCode,
15 server_message: Option<String>,
16 contexts: Vec<String>,
17 },
18
19 Context {
21 contexts: Vec<String>,
22 #[source]
23 source: Box<HapiError>,
24 },
25
26 NullByte(#[from] std::ffi::NulError),
28
29 Utf8(#[from] std::string::FromUtf8Error),
31
32 Io(#[from] std::io::Error),
34
35 Internal(String),
37}
38
39#[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
75impl 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
166impl 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 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 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 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 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 assert!(s.starts_with("[INVALID_ARGUMENT]"));
339 assert!(s.contains("\n\t0. inner\n\t1. outer\n"));
341 }
342}