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}