Skip to main content

uika_runtime/
error.rs

1// Error types for the Uika runtime.
2
3use std::fmt;
4
5use uika_ffi::UikaErrorCode;
6
7/// Rich error type for Uika operations.
8#[derive(Debug)]
9pub enum UikaError {
10    ObjectDestroyed,
11    InvalidCast,
12    PropertyNotFound(String),
13    FunctionNotFound(String),
14    TypeMismatch,
15    NullArgument,
16    IndexOutOfRange,
17    InvalidOperation(String),
18    Internal(String),
19    BufferTooSmall,
20}
21
22impl fmt::Display for UikaError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            UikaError::ObjectDestroyed => write!(f, "object has been destroyed"),
26            UikaError::InvalidCast => write!(f, "invalid cast"),
27            UikaError::PropertyNotFound(name) => write!(f, "property not found: {name}"),
28            UikaError::FunctionNotFound(name) => write!(f, "function not found: {name}"),
29            UikaError::TypeMismatch => write!(f, "type mismatch"),
30            UikaError::NullArgument => write!(f, "null argument"),
31            UikaError::IndexOutOfRange => write!(f, "index out of range"),
32            UikaError::InvalidOperation(msg) => write!(f, "invalid operation: {msg}"),
33            UikaError::Internal(msg) => write!(f, "internal error: {msg}"),
34            UikaError::BufferTooSmall => write!(f, "buffer too small"),
35        }
36    }
37}
38
39impl std::error::Error for UikaError {}
40
41/// Convenience alias used throughout the runtime and generated code.
42pub type UikaResult<T> = Result<T, UikaError>;
43
44/// Convert an FFI error code to a `UikaResult<()>`.
45/// `Ok` maps to `Ok(())`, all others map to the corresponding `UikaError`.
46pub fn check_ffi(code: UikaErrorCode) -> UikaResult<()> {
47    match code {
48        UikaErrorCode::Ok => Ok(()),
49        other => Err(UikaError::from(other)),
50    }
51}
52
53/// Like `check_ffi`, but enriches property/function errors with the given name.
54pub fn check_ffi_ctx(code: UikaErrorCode, context: &str) -> UikaResult<()> {
55    match code {
56        UikaErrorCode::Ok => Ok(()),
57        UikaErrorCode::PropertyNotFound => Err(UikaError::PropertyNotFound(context.into())),
58        UikaErrorCode::FunctionNotFound => Err(UikaError::FunctionNotFound(context.into())),
59        UikaErrorCode::InvalidOperation => Err(UikaError::InvalidOperation(context.into())),
60        other => Err(UikaError::from(other)),
61    }
62}
63
64/// Assert that an FFI call returned `Ok`. Used for codegen-generated methods
65/// where handle validation has already been performed and the C++ wrapper
66/// is expected to always succeed. Panics in debug builds if the code is not `Ok`.
67#[inline(always)]
68pub fn ffi_infallible(code: UikaErrorCode) {
69    debug_assert_eq!(
70        code,
71        UikaErrorCode::Ok,
72        "FFI call returned {:?} after pre-validation",
73        code
74    );
75}
76
77/// Like [`ffi_infallible`], but includes a context string in the panic message.
78#[inline(always)]
79pub fn ffi_infallible_ctx(code: UikaErrorCode, ctx: &str) {
80    debug_assert_eq!(
81        code,
82        UikaErrorCode::Ok,
83        "FFI '{}' returned {:?} after pre-validation",
84        ctx,
85        code
86    );
87}
88
89impl From<UikaErrorCode> for UikaError {
90    #[allow(clippy::match_same_arms)]
91    fn from(code: UikaErrorCode) -> Self {
92        match code {
93            UikaErrorCode::Ok => {
94                // Callers should not convert Ok into an error. If they do,
95                // treat it as an internal logic bug.
96                UikaError::Internal("unexpected Ok error code".into())
97            }
98            UikaErrorCode::ObjectDestroyed => UikaError::ObjectDestroyed,
99            UikaErrorCode::InvalidCast => UikaError::InvalidCast,
100            UikaErrorCode::PropertyNotFound => UikaError::PropertyNotFound(String::new()),
101            UikaErrorCode::FunctionNotFound => UikaError::FunctionNotFound(String::new()),
102            UikaErrorCode::TypeMismatch => UikaError::TypeMismatch,
103            UikaErrorCode::NullArgument => UikaError::NullArgument,
104            UikaErrorCode::IndexOutOfRange => UikaError::IndexOutOfRange,
105            UikaErrorCode::InvalidOperation => UikaError::InvalidOperation(String::new()),
106            UikaErrorCode::InternalError => UikaError::Internal(String::new()),
107            UikaErrorCode::BufferTooSmall => UikaError::BufferTooSmall,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn check_ffi_ok_returns_ok() {
118        assert!(check_ffi(UikaErrorCode::Ok).is_ok());
119    }
120
121    #[test]
122    fn check_ffi_errors_map_correctly() {
123        let cases = [
124            (UikaErrorCode::ObjectDestroyed, "ObjectDestroyed"),
125            (UikaErrorCode::InvalidCast, "InvalidCast"),
126            (UikaErrorCode::PropertyNotFound, "PropertyNotFound"),
127            (UikaErrorCode::FunctionNotFound, "FunctionNotFound"),
128            (UikaErrorCode::TypeMismatch, "TypeMismatch"),
129            (UikaErrorCode::NullArgument, "NullArgument"),
130            (UikaErrorCode::IndexOutOfRange, "IndexOutOfRange"),
131            (UikaErrorCode::InvalidOperation, "InvalidOperation"),
132            (UikaErrorCode::InternalError, "Internal"),
133        ];
134        for (code, expected_variant) in cases {
135            let err = check_ffi(code).unwrap_err();
136            let debug = format!("{err:?}");
137            assert!(
138                debug.starts_with(expected_variant),
139                "expected {expected_variant}, got {debug}"
140            );
141        }
142    }
143
144    #[test]
145    fn display_formats_are_human_readable() {
146        let err = UikaError::PropertyNotFound("Health".into());
147        assert_eq!(err.to_string(), "property not found: Health");
148    }
149}