Skip to main content

turbovault_plugin_api/
error.rs

1use serde::{Deserialize, Serialize};
2
3/// Stable, machine-readable categories returned across the plugin boundary.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6#[non_exhaustive]
7pub enum PluginErrorCode {
8    /// A request was malformed or violated a contract precondition.
9    InvalidInput,
10    /// The requested vault, note, or tool does not exist.
11    NotFound,
12    /// An optimistic-concurrency precondition failed.
13    Conflict,
14    /// The plugin did not declare the capability its request needs.
15    ///
16    /// Distinct from [`Self::InvalidInput`]: the request was well-formed and
17    /// the target may well exist — the plugin is simply not permitted to reach
18    /// it, and no retry or reformulation will change that.
19    PermissionDenied,
20    /// A required host capability is temporarily unavailable.
21    Unavailable,
22    /// The plugin exceeded the host's time budget for a single call.
23    Timeout,
24    /// The host or plugin failed unexpectedly.
25    Internal,
26}
27
28/// Error type shared by the host and plugins.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
30#[error("{code:?}: {message}")]
31pub struct PluginError {
32    /// Stable error category.
33    pub code: PluginErrorCode,
34    /// Human-readable detail safe to return to a plugin caller.
35    pub message: String,
36}
37
38impl PluginError {
39    /// Construct an error with a stable code.
40    pub fn new(code: PluginErrorCode, message: impl Into<String>) -> Self {
41        Self {
42            code,
43            message: message.into(),
44        }
45    }
46
47    /// Construct an invalid-input error.
48    pub fn invalid_input(message: impl Into<String>) -> Self {
49        Self::new(PluginErrorCode::InvalidInput, message)
50    }
51
52    /// Construct a not-found error.
53    pub fn not_found(message: impl Into<String>) -> Self {
54        Self::new(PluginErrorCode::NotFound, message)
55    }
56
57    /// Construct a concurrency-conflict error.
58    pub fn conflict(message: impl Into<String>) -> Self {
59        Self::new(PluginErrorCode::Conflict, message)
60    }
61
62    /// Construct an undeclared-capability error.
63    pub fn permission_denied(message: impl Into<String>) -> Self {
64        Self::new(PluginErrorCode::PermissionDenied, message)
65    }
66
67    /// Construct an unavailable error.
68    pub fn unavailable(message: impl Into<String>) -> Self {
69        Self::new(PluginErrorCode::Unavailable, message)
70    }
71
72    /// Construct a call-budget-exceeded error.
73    pub fn timeout(message: impl Into<String>) -> Self {
74        Self::new(PluginErrorCode::Timeout, message)
75    }
76
77    /// Construct an internal error.
78    pub fn internal(message: impl Into<String>) -> Self {
79        Self::new(PluginErrorCode::Internal, message)
80    }
81}
82
83/// Result type used by plugin contracts.
84pub type PluginResult<T> = Result<T, PluginError>;