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 AlreadyInitialized, AssetDefAlreadyLoaded, AssetInvalid, CantGeneratePreset,
47 CantLoadGeo, CantLoadPreset, CantLoadfile, DisallowedHengineindieW3partyPlugin,
48 DisallowedLcAssetWithCLicense, DisallowedNcAssetWithCLicense,
49 DisallowedNcAssetWithLcLicense, DisallowedNcLicenseFound, Failure, InvalidArgument,
50 InvalidSession, InvalidSharedMemoryBuffer, NoLicenseFound, NodeInvalid, NotInitialized,
51 ParmSetFailed, SharedMemoryBufferOverflow, Success, UserInterrupted,
52 };
53 let desc = match self.0 {
54 Success => "SUCCESS",
55 Failure => "FAILURE",
56 AlreadyInitialized => "ALREADY_INITIALIZED",
57 NotInitialized => "NOT_INITIALIZED",
58 CantLoadfile => "CANT_LOADFILE",
59 ParmSetFailed => "PARM_SET_FAILED",
60 InvalidArgument => "INVALID_ARGUMENT",
61 CantLoadGeo => "CANT_LOAD_GEO",
62 CantGeneratePreset => "CANT_GENERATE_PRESET",
63 CantLoadPreset => "CANT_LOAD_PRESET",
64 AssetDefAlreadyLoaded => "ASSET_DEF_ALREADY_LOADED",
65 NoLicenseFound => "NO_LICENSE_FOUND",
66 DisallowedNcLicenseFound => "DISALLOWED_NC_LICENSE_FOUND",
67 DisallowedNcAssetWithCLicense => "DISALLOWED_NC_ASSET_WITH_C_LICENSE",
68 DisallowedNcAssetWithLcLicense => "DISALLOWED_NC_ASSET_WITH_LC_LICENSE",
69 DisallowedLcAssetWithCLicense => "DISALLOWED_LC_ASSET_WITH_C_LICENSE",
70 DisallowedHengineindieW3partyPlugin => "DISALLOWED_HENGINEINDIE_W_3PARTY_PLUGIN",
71 AssetInvalid => "ASSET_INVALID",
72 NodeInvalid => "NODE_INVALID",
73 UserInterrupted => "USER_INTERRUPTED",
74 InvalidSession => "INVALID_SESSION",
75 SharedMemoryBufferOverflow => "SHARED_MEMORY_BUFFER_OVERFLOW",
76 InvalidSharedMemoryBuffer => "INVALID_SHARED_MEMORY_BUFFER",
77 };
78 write!(f, "{desc}")
79 }
80}
81
82impl From<std::convert::Infallible> for HapiError {
85 fn from(_: std::convert::Infallible) -> Self {
86 unreachable!()
87 }
88}
89
90impl From<HapiResult> for HapiError {
91 fn from(r: HapiResult) -> Self {
92 HapiError::Hapi {
93 result_code: HapiResultCode(r),
94 server_message: None,
95 contexts: Vec::new(),
96 }
97 }
98}
99
100impl From<&str> for HapiError {
101 fn from(value: &str) -> Self {
102 HapiError::Internal(value.to_string())
103 }
104}
105
106pub(crate) trait ErrorContext<T> {
107 fn context<C>(self, context: C) -> Result<T>
108 where
109 C: Into<String>;
110
111 #[allow(unused)]
112 fn with_context<C, F>(self, func: F) -> Result<T>
113 where
114 C: Into<String>,
115 F: FnOnce() -> C;
116}
117
118impl<T> ErrorContext<T> for Result<T> {
119 fn context<C>(self, context: C) -> Result<T>
120 where
121 C: Into<String>,
122 {
123 match self {
124 Ok(ok) => Ok(ok),
125 Err(mut error) => {
126 let context = context.into();
127 match &mut error {
128 HapiError::Hapi { contexts, .. } | HapiError::Context { contexts, .. } => {
129 contexts.push(context);
130 Err(error)
131 }
132 _ => Err(HapiError::Context {
133 contexts: vec![context],
134 source: Box::new(error),
135 }),
136 }
137 }
138 }
139 }
140
141 fn with_context<C, F>(self, func: F) -> Result<T>
142 where
143 C: Into<String>,
144 F: FnOnce() -> C,
145 {
146 match self {
147 Ok(ok) => Ok(ok),
148 Err(mut error) => {
149 let context = func().into();
150 match &mut error {
151 HapiError::Hapi { contexts, .. } | HapiError::Context { contexts, .. } => {
152 contexts.push(context);
153 Err(error)
154 }
155 _ => Err(HapiError::Context {
156 contexts: vec![context],
157 source: Box::new(error),
158 }),
159 }
160 }
161 }
162 }
163}
164
165impl std::fmt::Display for HapiError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 fn fmt_base(err: &HapiError, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match err {
170 HapiError::Hapi {
171 result_code,
172 server_message,
173 ..
174 } => {
175 write!(f, "[{result_code}]")?;
176 if let Some(msg) = server_message {
177 write!(f, ": [Engine Message]: {msg}")?;
178 }
179 Ok(())
180 }
181 HapiError::Context { source, .. } => fmt_base(source, f),
182 HapiError::NullByte(e) => {
183 let vec = e.clone().into_vec();
184 let text = String::from_utf8_lossy(&vec);
185 write!(f, "String contains null byte in \"{text}\"")
186 }
187 HapiError::Utf8(e) => {
188 let text = String::from_utf8_lossy(e.as_bytes());
189 write!(f, "Invalid UTF-8 in string \"{text}\"")
190 }
191 HapiError::Io(e) => write!(f, "IO error: {e}"),
192 HapiError::Internal(e) => write!(f, "Internal error: {e}"),
193 }
194 }
195
196 fn collect_contexts<'a>(err: &'a HapiError, out: &mut Vec<&'a str>) {
197 match err {
198 HapiError::Hapi { contexts, .. } => {
199 out.extend(contexts.iter().map(std::string::String::as_str));
200 }
201 HapiError::Context { contexts, source } => {
202 collect_contexts(source, out);
203 out.extend(contexts.iter().map(std::string::String::as_str));
204 }
205 _ => {}
206 }
207 }
208
209 fmt_base(self, f)?;
210
211 let mut contexts = Vec::new();
212 collect_contexts(self, &mut contexts);
213 if !contexts.is_empty() {
214 writeln!(f)?;
215 for (n, msg) in contexts.iter().enumerate() {
216 writeln!(f, "\t{n}. {msg}")?;
217 }
218 }
219 Ok(())
220 }
221}
222
223impl HapiResult {
224 pub(crate) fn check_err<F, M>(self, session: &Session, context: F) -> Result<()>
226 where
227 M: Into<String>,
228 F: FnOnce() -> M,
229 {
230 match self {
231 HapiResult::Success => Ok(()),
232 _err => {
233 let server_message = if session.is_valid() {
234 session
235 .get_status_string(StatusType::CallResult, StatusVerbosity::All)
236 .ok()
237 } else {
238 Some("Session is corrupted: error message not available".to_string())
239 };
240
241 Err(HapiError::Hapi {
242 result_code: HapiResultCode(self),
243 server_message: server_message
244 .or_else(|| Some("Could not retrieve error message".to_string())),
245 contexts: vec![context().into()],
246 })
247 }
248 }
249 }
250
251 pub(crate) fn add_context<I: Into<String>>(self, message: I) -> Result<()> {
253 match self {
254 HapiResult::Success => Ok(()),
255 _err => Err(HapiError::Hapi {
256 result_code: HapiResultCode(self),
257 server_message: None,
258 contexts: vec![message.into()],
259 }),
260 }
261 }
262
263 pub(crate) fn with_context<F, M>(self, func: F) -> Result<()>
264 where
265 F: FnOnce() -> M,
266 M: Into<String>,
267 {
268 self.add_context(func())
269 }
270
271 pub(crate) fn with_server_message<F, M>(self, func: F) -> Result<()>
272 where
273 F: FnOnce() -> M,
274 M: Into<String>,
275 {
276 match self {
277 HapiResult::Success => Ok(()),
278 _err => Err(HapiError::Hapi {
279 result_code: HapiResultCode(self),
280 server_message: Some(func().into()),
281 contexts: vec![],
282 }),
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::error::Error as _;
291
292 #[test]
293 fn context_chain_is_rendered_for_internal_errors() {
294 let err = (Err::<(), HapiError>(HapiError::Internal("root".to_string())))
295 .context("first context")
296 .context("second context")
297 .unwrap_err();
298
299 let s = err.to_string();
300 assert!(s.starts_with("Internal error: root"));
301 assert!(s.contains("\n\t0. first context\n\t1. second context\n"));
302
303 let source = err.source().expect("source");
305 assert_eq!(source.to_string(), "Internal error: root");
306 }
307
308 #[test]
309 fn hapi_errors_server_message_and_contexts() {
310 let err = (Err::<(), HapiError>(HapiError::Hapi {
311 result_code: HapiResultCode(HapiResult::Failure),
312 server_message: Some("could not cook".to_string()),
313 contexts: vec!["low-level".to_string()],
314 }))
315 .context("high-level")
316 .unwrap_err();
317
318 let s = err.to_string();
319 assert_eq!(
320 s,
321 "[FAILURE]: [Engine Message]: could not cook\n\t0. low-level\n\t1. high-level\n"
322 );
323 }
324
325 #[test]
326 fn context_added_outside_hapi_error_is_rendered_after_inner_contexts() {
327 let base = HapiError::Hapi {
329 result_code: HapiResultCode(HapiResult::InvalidArgument),
330 server_message: None,
331 contexts: vec!["inner".to_string()],
332 };
333 let wrapped = (Err::<(), HapiError>(base)).context("outer").unwrap_err();
334
335 let s = wrapped.to_string();
336 assert!(s.starts_with("[INVALID_ARGUMENT]"));
338 assert!(s.contains("\n\t0. inner\n\t1. outer\n"));
340 }
341
342 #[test]
343 fn result_with_context_adds_context_on_error() {
344 let err = Err::<(), HapiError>(HapiError::Internal("root".to_string()))
345 .with_context(|| "deferred context")
346 .unwrap_err();
347
348 assert_eq!(
349 err.to_string(),
350 "Internal error: root\n\t0. deferred context\n"
351 );
352 assert_eq!(
353 err.source().expect("source").to_string(),
354 "Internal error: root"
355 );
356 }
357
358 #[test]
359 fn hapi_result_add_context_returns_hapi_error_on_failure() {
360 let err = HapiResult::InvalidArgument
361 .add_context("invalid parm")
362 .unwrap_err();
363
364 match err {
365 HapiError::Hapi {
366 result_code,
367 server_message,
368 contexts,
369 } => {
370 assert_eq!(result_code.to_string(), "INVALID_ARGUMENT");
371 assert_eq!(server_message, None);
372 assert_eq!(contexts, vec!["invalid parm"]);
373 }
374 other => panic!("expected Hapi error, got {other:?}"),
375 }
376 }
377
378 #[test]
379 fn hapi_result_with_context_returns_hapi_error_on_failure() {
380 let err = HapiResult::Failure
381 .with_context(|| "deferred hapi context")
382 .unwrap_err();
383
384 assert_eq!(err.to_string(), "[FAILURE]\n\t0. deferred hapi context\n");
385 }
386
387 #[test]
388 fn hapi_result_with_server_message_returns_hapi_error_on_failure() {
389 let err = HapiResult::CantLoadfile
390 .with_server_message(|| "could not load asset")
391 .unwrap_err();
392
393 match err {
394 HapiError::Hapi {
395 result_code,
396 server_message,
397 contexts,
398 } => {
399 assert_eq!(result_code.to_string(), "CANT_LOADFILE");
400 assert_eq!(server_message, Some("could not load asset".to_string()));
401 assert!(contexts.is_empty());
402 }
403 other => panic!("expected Hapi error, got {other:?}"),
404 }
405 }
406
407 #[test]
408 fn null_byte_errors_are_rendered() {
409 let err = std::ffi::CString::new(b"ab\0cd".to_vec()).unwrap_err();
410 let err = HapiError::from(err);
411
412 assert_eq!(err.to_string(), "String contains null byte in \"ab\0cd\"");
413 }
414
415 #[test]
416 fn utf8_errors_are_rendered() {
417 let err = String::from_utf8(vec![b'a', 0xff, b'b']).unwrap_err();
418 let err = HapiError::from(err);
419
420 assert_eq!(err.to_string(), "Invalid UTF-8 in string \"a\u{FFFD}b\"");
421 }
422
423 #[test]
424 fn io_errors_are_rendered() {
425 let err = HapiError::from(std::io::Error::new(
426 std::io::ErrorKind::NotFound,
427 "missing file",
428 ));
429
430 assert_eq!(err.to_string(), "IO error: missing file");
431 }
432
433 #[test]
434 fn str_converts_to_internal_error() {
435 let err = HapiError::from("bad state");
436
437 assert_eq!(err.to_string(), "Internal error: bad state");
438 }
439
440 #[test]
441 fn hapi_result_converts_to_hapi_error() {
442 let err = HapiError::from(HapiResult::ParmSetFailed);
443
444 match err {
445 HapiError::Hapi {
446 result_code,
447 server_message,
448 contexts,
449 } => {
450 assert_eq!(result_code.to_string(), "PARM_SET_FAILED");
451 assert_eq!(server_message, None);
452 assert!(contexts.is_empty());
453 }
454 other => panic!("expected Hapi error, got {other:?}"),
455 }
456 }
457}