Skip to main content

confium_api/
error.rs

1//! Error model exposed to plugin authors.
2//!
3//! Confium plugins return a `u32` status code from each FFI entry point.
4//! `0` means success; any other value is one of the canonical codes from
5//! [`ErrorCode`]. Plugin authors do not want to memorize those numbers —
6//! they want to return their own error type and let the SDK convert.
7//!
8//! [`PluginError`] is the SDK-side enum plugin authors implement `From`
9//! for (or use directly). It carries the canonical [`ErrorCode`] so the
10//! macro-generated entry points can `map_or_else` it into the wire `u32`
11//! exactly the way the core crate does.
12
13use std::ffi::c_void;
14
15/// Canonical error codes returned across the Confium FFI surface.
16///
17/// These mirror `confium_core::error::ErrorCode` exactly — same names,
18/// same numeric values — so a plugin's status code is meaningful to the
19/// loader without a translation layer. (The cross-crate link is not
20/// resolvable here because `confium-api` does not depend on
21/// `confium-core`.)
22///
23/// Never reorder or renumber existing variants: they are wire-stable.
24/// New variants may only be appended.
25#[repr(u32)]
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[allow(non_camel_case_types)]
28#[non_exhaustive]
29pub enum ErrorCode {
30    UNKNOWN = 1,
31    NULL_POINTER = 2,
32    INVALID_UTF8 = 3,
33
34    WRONG_TYPE = 10,
35    VALUE_NOT_FOUND = 11,
36    INSUFFICIENT_BUFFER = 12,
37
38    UNKNOWN_PROVIDER = 13,
39
40    PLUGIN_LOAD_FAILED = 20,
41    PLUGIN_SYMBOL_ERROR = 21,
42    PLUGIN_INITIALIZATION_FAILED = 22,
43    PLUGIN_INTERFACE_VERSION_UNSUPPORTED = 23,
44    PLUGIN_NAME_COLLISION = 24,
45    PLUGIN_MISSING_INTERFACE = 25,
46    PLUGIN_INTERNAL_ERROR = 26,
47
48    UNSUPPORTED_ALGORITHM = 50,
49
50    /// The plugin encountered an error that does not map to one of the
51    /// canonical codes. The plugin should still produce a useful
52    /// `Display` string for logging, but the loader has no typed
53    /// recovery path.
54    PLUGIN_GENERIC = 27,
55}
56
57impl ErrorCode {
58    /// Numeric wire value the loader expects. Equal to the discriminant.
59    pub fn into_wire(self) -> u32 {
60        self as u32
61    }
62}
63
64impl From<ErrorCode> for u32 {
65    fn from(code: ErrorCode) -> Self {
66        code.into_wire()
67    }
68}
69
70/// Plugin-side error type. Carries a canonical [`ErrorCode`] plus a
71/// human-readable message for logging; only the code crosses the FFI.
72#[derive(Debug)]
73pub struct PluginError {
74    code: ErrorCode,
75    message: String,
76}
77
78impl PluginError {
79    /// Construct a new error with the given code and message.
80    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
81        Self {
82            code,
83            message: message.into(),
84        }
85    }
86
87    /// Shorthand for a generic error with no specific code.
88    pub fn msg(message: impl Into<String>) -> Self {
89        Self::new(ErrorCode::PLUGIN_GENERIC, message)
90    }
91
92    /// Canonical code that will be returned to the loader.
93    pub fn code(&self) -> ErrorCode {
94        self.code
95    }
96
97    /// Human-readable message. For logging only — does not cross the FFI.
98    pub fn message(&self) -> &str {
99        &self.message
100    }
101
102    /// Convert into the wire `u32` status code the loader expects.
103    pub fn into_wire(self) -> u32 {
104        self.code.into_wire()
105    }
106}
107
108impl std::fmt::Display for PluginError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}: {}", self.code_as_str(), self.message)
111    }
112}
113
114impl std::error::Error for PluginError {}
115
116impl PluginError {
117    fn code_as_str(&self) -> &'static str {
118        match self.code {
119            ErrorCode::UNKNOWN => "unknown",
120            ErrorCode::NULL_POINTER => "null_pointer",
121            ErrorCode::INVALID_UTF8 => "invalid_utf8",
122            ErrorCode::WRONG_TYPE => "wrong_type",
123            ErrorCode::VALUE_NOT_FOUND => "value_not_found",
124            ErrorCode::INSUFFICIENT_BUFFER => "insufficient_buffer",
125            ErrorCode::UNKNOWN_PROVIDER => "unknown_provider",
126            ErrorCode::PLUGIN_LOAD_FAILED => "plugin_load_failed",
127            ErrorCode::PLUGIN_SYMBOL_ERROR => "plugin_symbol_error",
128            ErrorCode::PLUGIN_INITIALIZATION_FAILED => "plugin_initialization_failed",
129            ErrorCode::PLUGIN_INTERFACE_VERSION_UNSUPPORTED => {
130                "plugin_interface_version_unsupported"
131            }
132            ErrorCode::PLUGIN_NAME_COLLISION => "plugin_name_collision",
133            ErrorCode::PLUGIN_MISSING_INTERFACE => "plugin_missing_interface",
134            ErrorCode::PLUGIN_INTERNAL_ERROR => "plugin_internal_error",
135            ErrorCode::UNSUPPORTED_ALGORITHM => "unsupported_algorithm",
136            ErrorCode::PLUGIN_GENERIC => "plugin_generic",
137        }
138    }
139}
140
141/// Result alias for plugin-side fallible operations.
142pub type PluginResult<T> = std::result::Result<T, PluginError>;
143
144/// Helper used by the macro-generated entry points to flatten a
145/// [`PluginResult<()>`] into the wire `u32` the loader expects.
146///
147/// Plugins do not normally call this directly — the macros do.
148#[doc(hidden)]
149pub fn to_wire_code<T>(result: PluginResult<T>) -> u32 {
150    match result {
151        Ok(_) => 0,
152        Err(e) => e.into_wire(),
153    }
154}
155
156/// Helper used by the macro-generated entry points to construct a
157/// `*mut c_void` plugin instance handle from a boxed Rust value.
158///
159/// Re-exports [`crate::OpaqueHandle::new`] so macro output doesn't need
160/// to fully-qualify the path.
161#[doc(hidden)]
162pub fn box_state<T>(value: T) -> *mut c_void {
163    crate::OpaqueHandle::new(value)
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn error_code_into_wire_matches_discriminant() {
172        assert_eq!(ErrorCode::UNKNOWN.into_wire(), 1);
173        assert_eq!(ErrorCode::PLUGIN_GENERIC.into_wire(), 27);
174        assert_eq!(ErrorCode::UNSUPPORTED_ALGORITHM.into_wire(), 50);
175    }
176
177    #[test]
178    fn plugin_error_carries_code_and_message() {
179        let e = PluginError::new(ErrorCode::INSUFFICIENT_BUFFER, "need 32 bytes");
180        assert_eq!(e.code(), ErrorCode::INSUFFICIENT_BUFFER);
181        assert_eq!(e.message(), "need 32 bytes");
182        let s = format!("{e}");
183        assert!(s.contains("insufficient_buffer"));
184        assert!(s.contains("need 32 bytes"));
185    }
186
187    #[test]
188    fn to_wire_code_success_is_zero() {
189        let r: PluginResult<()> = Ok(());
190        assert_eq!(to_wire_code(r), 0);
191    }
192
193    #[test]
194    fn to_wire_code_failure_is_error_code() {
195        let r: PluginResult<()> = Err(PluginError::new(ErrorCode::NULL_POINTER, "x"));
196        assert_eq!(to_wire_code(r), ErrorCode::NULL_POINTER.into_wire());
197    }
198}