Skip to main content

uni_plugin/
plugin.rs

1//! The core `Plugin` trait and supporting types.
2//!
3//! Every uni-db extension implements [`Plugin`]. The trait is deliberately
4//! tiny: a [`PluginManifest`] accessor, a `register` method that calls into a
5//! `PluginRegistrar`, and optional `init` / `shutdown` hooks. The heavy
6//! lifting lives in the per-surface capability traits in [`crate::traits`].
7
8use std::sync::Arc;
9
10use serde::{Deserialize, Serialize};
11use smol_str::SmolStr;
12
13use crate::errors::PluginError;
14use crate::manifest::PluginManifest;
15use crate::registrar::PluginRegistrar;
16
17/// Reverse-DNS plugin identifier — e.g. `"ai.dragonscale.geo"`.
18///
19/// Used as the namespace component of every [`crate::QName`] the plugin
20/// registers. Must be unique across all plugins loaded into one Uni
21/// instance.
22#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct PluginId(SmolStr);
25
26impl PluginId {
27    /// Construct a `PluginId` from a string.
28    ///
29    /// # Panics
30    ///
31    /// Panics if `s` is empty — a programming error, since plugin ids are
32    /// determined at plugin-author time.
33    #[must_use]
34    pub fn new(s: impl Into<SmolStr>) -> Self {
35        let s = s.into();
36        assert!(!s.is_empty(), "PluginId must not be empty");
37        Self(s)
38    }
39
40    /// Parse a plugin id from a string slice.
41    ///
42    /// Currently the same as [`PluginId::new`] but returns a `Result` to
43    /// keep the API forward-compatible with future validation rules
44    /// (reserved prefixes, character restrictions, length caps).
45    ///
46    /// # Errors
47    ///
48    /// Returns [`PluginError::Internal`] if the input is empty.
49    pub fn parse(s: impl AsRef<str>) -> Result<Self, PluginError> {
50        let s = s.as_ref();
51        if s.is_empty() {
52            return Err(PluginError::internal("PluginId must not be empty"));
53        }
54        Ok(Self(SmolStr::new(s)))
55    }
56
57    /// Returns the string representation.
58    #[must_use]
59    pub fn as_str(&self) -> &str {
60        &self.0
61    }
62}
63
64impl std::fmt::Display for PluginId {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.write_str(&self.0)
67    }
68}
69
70/// A handle returned by `Uni::add_plugin` and similar APIs.
71///
72/// Carries the plugin id plus a *generation* counter that bumps on every
73/// hot-reload. Holding a `PluginHandle` does not keep the plugin alive
74/// against `remove_plugin` — the handle simply identifies which plugin's
75/// registrations are being targeted.
76#[derive(Clone, Debug, PartialEq, Eq, Hash)]
77pub struct PluginHandle {
78    /// Plugin id at registration.
79    pub id: PluginId,
80    /// Hot-reload generation (`0` on initial load, increments on each
81    /// successful reload).
82    pub generation: u64,
83}
84
85impl PluginHandle {
86    /// Construct a handle.
87    #[must_use]
88    pub fn new(id: PluginId, generation: u64) -> Self {
89        Self { id, generation }
90    }
91}
92
93/// Init-time context provided to [`Plugin::init`].
94///
95/// Currently carries the effective capability set; future fields may add
96/// host-version information, configured workspace paths, etc.
97#[derive(Debug)]
98#[non_exhaustive]
99pub struct PluginInitContext<'a> {
100    /// The capability set after intersecting manifest-requested with
101    /// host-granted.
102    pub effective_caps: &'a crate::CapabilitySet,
103}
104
105/// Marker trait for plugin-side extension state.
106///
107/// Mostly an implementation hint: plugin-shared mutable state should be held
108/// behind `Arc<…>` and `Send + Sync`. This trait does not enforce anything
109/// but documents the expectation.
110pub trait PluginState: Send + Sync + 'static {}
111
112impl<T: Send + Sync + 'static> PluginState for T {}
113
114/// The trait every uni-db extension implements.
115///
116/// A `Plugin` is a *bundle* of capability registrations: scalar functions,
117/// aggregates, procedures, hooks, etc. The trait itself is small; all
118/// per-surface detail lives in the capability traits in [`crate::traits`].
119///
120/// # Examples
121///
122/// ```no_run
123/// use std::sync::Arc;
124/// use uni_plugin::{Plugin, PluginManifest, PluginRegistrar, PluginError};
125///
126/// pub struct NoopPlugin {
127///     manifest: PluginManifest,
128/// }
129///
130/// impl Plugin for NoopPlugin {
131///     fn manifest(&self) -> &PluginManifest { &self.manifest }
132///     fn register(&self, _r: &mut PluginRegistrar<'_>) -> Result<(), PluginError> {
133///         Ok(())
134///     }
135/// }
136/// ```
137pub trait Plugin: Send + Sync + 'static {
138    /// Static plugin description.
139    ///
140    /// Implementations typically store the manifest in a field and return a
141    /// borrow. The manifest is read at load time to compute the effective
142    /// capability set before [`Plugin::register`] is invoked.
143    fn manifest(&self) -> &PluginManifest;
144
145    /// Register extension points with the host.
146    ///
147    /// Called exactly once at load time, after capability negotiation. The
148    /// plugin uses the registrar's typed builder methods to claim qualified
149    /// names for each kind of extension.
150    ///
151    /// # Errors
152    ///
153    /// Returns [`PluginError::DuplicateRegistration`] if two registrations
154    /// claim the same `QName`, or [`PluginError::CapabilityRequired`] if a
155    /// registration requires a capability not in the effective set.
156    fn register(&self, r: &mut PluginRegistrar<'_>) -> Result<(), PluginError>;
157
158    /// Optional initialization callback.
159    ///
160    /// Called once after registration, in dependency order over the
161    /// manifest's `depends_on` list. The default no-op is sufficient for
162    /// plugins that don't need init-time setup.
163    ///
164    /// # Errors
165    ///
166    /// Plugins that fail initialization should return a [`PluginError`];
167    /// the host will then unregister this plugin's registrations and
168    /// propagate the error to the caller of `Uni::add_plugin`.
169    fn init(&self, _cx: &PluginInitContext<'_>) -> Result<(), PluginError> {
170        Ok(())
171    }
172
173    /// Optional shutdown callback.
174    ///
175    /// Called once at instance teardown, in reverse dependency order. The
176    /// default does nothing; plugins holding external resources (open
177    /// files, network connections) override this for graceful cleanup.
178    fn shutdown(&self) {}
179}
180
181/// A type-erased plugin reference suitable for storing in collections.
182pub type DynPlugin = Arc<dyn Plugin>;
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn plugin_id_round_trip() {
190        let id = PluginId::new("ai.dragonscale.geo");
191        assert_eq!(id.as_str(), "ai.dragonscale.geo");
192        assert_eq!(id.to_string(), "ai.dragonscale.geo");
193    }
194
195    #[test]
196    #[should_panic(expected = "PluginId must not be empty")]
197    fn plugin_id_empty_panics() {
198        let _ = PluginId::new("");
199    }
200
201    #[test]
202    fn plugin_id_parse_rejects_empty() {
203        assert!(PluginId::parse("").is_err());
204    }
205
206    #[test]
207    fn plugin_handle_construction() {
208        let h = PluginHandle::new(PluginId::new("foo"), 0);
209        assert_eq!(h.id.as_str(), "foo");
210        assert_eq!(h.generation, 0);
211    }
212}