Skip to main content

elevator_core/
host_error.rs

1//! Shared error classification for host bindings.
2//!
3//! Each host crate (FFI, wasm, gdext, Bevy) translates simulation
4//! failures into its own idiomatic error shape — `EvStatus` integers
5//! for FFI, `JsValue`-shaped exceptions for wasm, Godot exceptions
6//! for gdext. Without a shared classification the bindings used to
7//! enumerate the *kinds* of failures separately, drifting whenever a
8//! new failure mode landed.
9//!
10//! [`ErrorKind`](crate::host_error::ErrorKind) is the shared
11//! vocabulary. Hosts map it to their native error type (FFI provides
12//! `From<ErrorKind> for EvStatus`).
13//!
14//! See [Host Binding Parity](https://andymai.github.io/elevator-core/host-binding-parity.html)
15//! for the wider cross-host contract this enum is part of.
16
17use serde::{Deserialize, Serialize};
18
19/// Classification of failures every host binding can surface.
20///
21/// Variants mirror FFI's historical `EvStatus` enum (minus `Ok`,
22/// which represents *success* rather than an error kind). Hosts
23/// that can't yet emit a given variant should still match against
24/// the full set with a `_` fallback so future variants don't break
25/// existing call sites.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[non_exhaustive]
28pub enum ErrorKind {
29    /// A required pointer / handle argument was null.
30    NullArg,
31    /// A C string argument was not valid UTF-8.
32    InvalidUtf8,
33    /// A config file could not be read from disk.
34    ConfigLoad,
35    /// A config file failed to parse.
36    ConfigParse,
37    /// `SimulationBuilder::build` rejected the resolved config.
38    BuildFailed,
39    /// The referenced entity, group, or resource was not found.
40    NotFound,
41    /// The argument was structurally valid but semantically rejected.
42    InvalidArg,
43    /// A Rust panic was caught at the host boundary; the underlying
44    /// state may be partially mutated and the host handle should be
45    /// considered unsafe to reuse.
46    Panic,
47}
48
49impl ErrorKind {
50    /// Stable string label for a variant.
51    ///
52    /// Hosts that need to surface the kind to a non-Rust consumer
53    /// (e.g. wasm's JS side, gdext's `GDScript` side) can use this to
54    /// produce a kebab-case label without paying for full
55    /// `Debug` rendering. The set of returned strings is part of
56    /// the cross-host contract — adding a new variant requires
57    /// adding a label here.
58    #[must_use]
59    pub const fn label(self) -> &'static str {
60        match self {
61            Self::NullArg => "null-arg",
62            Self::InvalidUtf8 => "invalid-utf8",
63            Self::ConfigLoad => "config-load",
64            Self::ConfigParse => "config-parse",
65            Self::BuildFailed => "build-failed",
66            Self::NotFound => "not-found",
67            Self::InvalidArg => "invalid-arg",
68            Self::Panic => "panic",
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::ErrorKind;
76
77    #[test]
78    fn label_is_kebab_case_and_stable() {
79        // Locked: changing any of these breaks consumer parsers.
80        assert_eq!(ErrorKind::NullArg.label(), "null-arg");
81        assert_eq!(ErrorKind::InvalidUtf8.label(), "invalid-utf8");
82        assert_eq!(ErrorKind::ConfigLoad.label(), "config-load");
83        assert_eq!(ErrorKind::ConfigParse.label(), "config-parse");
84        assert_eq!(ErrorKind::BuildFailed.label(), "build-failed");
85        assert_eq!(ErrorKind::NotFound.label(), "not-found");
86        assert_eq!(ErrorKind::InvalidArg.label(), "invalid-arg");
87        assert_eq!(ErrorKind::Panic.label(), "panic");
88    }
89
90    #[test]
91    fn labels_are_unique() {
92        let labels = [
93            ErrorKind::NullArg.label(),
94            ErrorKind::InvalidUtf8.label(),
95            ErrorKind::ConfigLoad.label(),
96            ErrorKind::ConfigParse.label(),
97            ErrorKind::BuildFailed.label(),
98            ErrorKind::NotFound.label(),
99            ErrorKind::InvalidArg.label(),
100            ErrorKind::Panic.label(),
101        ];
102        let unique: std::collections::HashSet<_> = labels.iter().collect();
103        assert_eq!(unique.len(), labels.len());
104    }
105}