Skip to main content

uni_plugin/
registrar.rs

1//! The [`PluginRegistrar`] a plugin's `register()` method calls.
2//!
3//! Every registration method is capability-gated against the effective
4//! capability set computed at load time (manifest-declared ∩ host-granted).
5//! Registrations claiming a `QName` that is already taken fail with
6//! [`crate::PluginError::DuplicateRegistration`].
7
8use std::sync::Arc;
9
10use smol_str::SmolStr;
11
12use crate::capability::{Capability, CapabilitySet};
13use crate::errors::PluginError;
14use crate::plugin::PluginId;
15use crate::qname::QName;
16use crate::registry::PluginRegistry;
17use crate::surfaces::{
18    AggregateSurface, AlgorithmSurface, AppendReg, AuthSurface, AuthzSurface, BackgroundJobSurface,
19    CatalogSurface, CdcSurface, CollationSurface, CrdtSurface, DynPendingRegistration, HookSurface,
20    IndexKindSurface, KeyedUniqueReg, LabelStorageSurface, LocyAggregateSurface,
21    LocyGeneratorSurface, LocyPredicateSurface, LogicalTypeSurface, NamedUniqueReg,
22    OptimizerRuleSurface, ProcedureSurface, ReplacementScanSurface, ScalarSurface, TriggerSurface,
23    VersionedReg, WindowSurface,
24};
25use crate::traits::aggregate::{AggSignature, AggregatePluginFn};
26use crate::traits::algorithm::AlgorithmProvider;
27use crate::traits::background::BackgroundJobProvider;
28use crate::traits::catalog::{CatalogProvider, ReplacementScanProvider};
29use crate::traits::cdc::CdcOutputProvider;
30use crate::traits::collation::CollationProvider;
31use crate::traits::connector::{AuthProvider, AuthzPolicy};
32use crate::traits::crdt::{CrdtKind, CrdtKindProvider};
33use crate::traits::hook::SessionHook;
34use crate::traits::index::{IndexKind, IndexKindProvider};
35use crate::traits::locy::{
36    GenSignature, LocyAggregate, LocyGenerator, LocyPredicate, PredSignature,
37};
38use crate::traits::operator::OptimizerRuleProvider;
39use crate::traits::procedure::{ProcedurePlugin, ProcedureSignature};
40use crate::traits::scalar::{FnSignature, ScalarPluginFn};
41use crate::traits::trigger::TriggerPlugin;
42use crate::traits::types::LogicalTypeProvider;
43use crate::traits::window::{WindowPluginFn, WindowSignature};
44
45/// The builder passed to [`crate::Plugin::register`].
46///
47/// Each registration method takes a [`QName`] and a trait-object
48/// implementation. The registrar verifies the corresponding capability is
49/// present in the effective set, rejects duplicate qnames, and forwards the
50/// registration to the [`PluginRegistry`].
51///
52/// The registrar is short-lived: one is created per `register()` call;
53/// changes flush to the [`PluginRegistry`] when `register()` returns
54/// successfully. A failed `register()` rolls back any partial state.
55pub struct PluginRegistrar<'a> {
56    plugin_id: PluginId,
57    effective_caps: &'a CapabilitySet,
58    registry: &'a PluginRegistry,
59    pending: Vec<Box<dyn DynPendingRegistration>>,
60    /// QNames of aggregate functions staged via [`Self::aggregate_fn`]. The
61    /// pending registrations are type-erased, so we record aggregate qnames
62    /// separately to let the host loader publish each one's Cypher
63    /// routing hint (`uni_cypher::register_plugin_aggregate`) after a
64    /// successful commit — without this, the Cypher planner classifies
65    /// `RETURN myAgg(x)` as a scalar UDF and fails to resolve it.
66    aggregate_qnames: Vec<QName>,
67}
68
69impl<'a> std::fmt::Debug for PluginRegistrar<'a> {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("PluginRegistrar")
72            .field("plugin_id", &self.plugin_id)
73            .field("pending", &self.pending.len())
74            .finish_non_exhaustive()
75    }
76}
77
78impl<'a> PluginRegistrar<'a> {
79    /// Construct a registrar for the given plugin.
80    ///
81    /// Created by the host loader; plugin authors never construct these
82    /// directly.
83    #[must_use]
84    pub fn new(
85        plugin_id: PluginId,
86        effective_caps: &'a CapabilitySet,
87        registry: &'a PluginRegistry,
88    ) -> Self {
89        Self {
90            plugin_id,
91            effective_caps,
92            registry,
93            pending: Vec::new(),
94            aggregate_qnames: Vec::new(),
95        }
96    }
97
98    /// QNames of aggregate functions staged on this registrar (in registration
99    /// order). The host loader uses these, after a successful
100    /// [`Self::commit_to_registry`], to publish each aggregate's Cypher
101    /// routing hint so `RETURN myAgg(x)` is planned as an aggregate rather
102    /// than a scalar UDF. Empty until [`Self::aggregate_fn`] is called.
103    #[must_use]
104    pub fn staged_aggregate_qnames(&self) -> &[QName] {
105        &self.aggregate_qnames
106    }
107
108    /// Returns the plugin id being registered.
109    #[must_use]
110    pub fn plugin_id(&self) -> &PluginId {
111        &self.plugin_id
112    }
113
114    /// Override the plugin id mid-registration.
115    ///
116    /// Used by external loaders (`uni-plugin-extism`, `uni-plugin-wasm`)
117    /// during their two-pass dance: pass 1 reads the plugin's
118    /// `manifest` export to learn the canonical id, then sets it here
119    /// so that `validate_qname` accepts qnames in the plugin's
120    /// declared namespace.
121    pub fn set_plugin_id(&mut self, plugin_id: PluginId) {
122        self.plugin_id = plugin_id;
123    }
124
125    fn require(&self, cap: &Capability) -> Result<(), PluginError> {
126        if self.effective_caps.contains_variant(cap) {
127            Ok(())
128        } else {
129            Err(PluginError::CapabilityRequired(cap.clone()))
130        }
131    }
132
133    fn validate_qname(&self, qname: &QName) -> Result<(), PluginError> {
134        if !qname.is_builtin() && qname.namespace() != self.plugin_id.as_str() {
135            return Err(PluginError::internal(format!(
136                "plugin `{}` cannot register qname `{}` outside its namespace",
137                self.plugin_id, qname
138            )));
139        }
140        Ok(())
141    }
142
143    /// Register a Cypher scalar function.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::ScalarFn`]
148    /// is absent, or [`PluginError::DuplicateRegistration`] (raised at
149    /// commit time) on qname collision.
150    pub fn scalar_fn(
151        &mut self,
152        qname: QName,
153        sig: FnSignature,
154        f: Arc<dyn ScalarPluginFn>,
155    ) -> Result<&mut Self, PluginError> {
156        self.require(&Capability::ScalarFn)?;
157        self.validate_qname(&qname)?;
158        self.pending.push(Box::new(NamedUniqueReg::<ScalarSurface> {
159            q: qname,
160            sig,
161            provider: f,
162        }));
163        Ok(self)
164    }
165
166    /// Register a Cypher aggregate function.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::AggregateFn`] is absent.
171    pub fn aggregate_fn(
172        &mut self,
173        qname: QName,
174        sig: AggSignature,
175        f: Arc<dyn AggregatePluginFn>,
176    ) -> Result<&mut Self, PluginError> {
177        self.require(&Capability::AggregateFn)?;
178        self.validate_qname(&qname)?;
179        self.aggregate_qnames.push(qname.clone());
180        self.pending
181            .push(Box::new(NamedUniqueReg::<AggregateSurface> {
182                q: qname,
183                sig,
184                provider: f,
185            }));
186        Ok(self)
187    }
188
189    /// Register a Cypher window function.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::WindowFn`] is absent.
194    pub fn window_fn(
195        &mut self,
196        qname: QName,
197        sig: WindowSignature,
198        f: Arc<dyn WindowPluginFn>,
199    ) -> Result<&mut Self, PluginError> {
200        self.require(&Capability::WindowFn)?;
201        self.validate_qname(&qname)?;
202        self.pending.push(Box::new(NamedUniqueReg::<WindowSurface> {
203            q: qname,
204            sig,
205            provider: f,
206        }));
207        Ok(self)
208    }
209
210    /// Register a Cypher procedure.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`PluginError::CapabilityRequired`] if the procedure's mode's
215    /// required capability is absent.
216    pub fn procedure(
217        &mut self,
218        qname: QName,
219        sig: ProcedureSignature,
220        p: Arc<dyn ProcedurePlugin>,
221    ) -> Result<&mut Self, PluginError> {
222        use crate::traits::procedure::ProcedureMode;
223        self.require(&Capability::Procedure)?;
224        match sig.mode {
225            ProcedureMode::Write => self.require(&Capability::ProcedureWrites)?,
226            ProcedureMode::Schema => self.require(&Capability::ProcedureSchema)?,
227            ProcedureMode::Dbms => self.require(&Capability::ProcedureDbms)?,
228            ProcedureMode::Read => {}
229        }
230        self.validate_qname(&qname)?;
231        self.pending
232            .push(Box::new(VersionedReg::<ProcedureSurface> {
233                q: qname,
234                sig,
235                provider: p,
236            }));
237        Ok(self)
238    }
239
240    /// Register a Locy aggregate.
241    ///
242    /// # Errors
243    ///
244    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::LocyAggregate`] is absent.
245    pub fn locy_aggregate(
246        &mut self,
247        qname: QName,
248        a: Arc<dyn LocyAggregate>,
249    ) -> Result<&mut Self, PluginError> {
250        self.require(&Capability::LocyAggregate)?;
251        self.validate_qname(&qname)?;
252        self.pending
253            .push(Box::new(NamedUniqueReg::<LocyAggregateSurface> {
254                q: qname,
255                sig: (),
256                provider: a,
257            }));
258        Ok(self)
259    }
260
261    /// Register a Locy predicate.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::LocyPredicate`] is absent.
266    pub fn locy_predicate(
267        &mut self,
268        qname: QName,
269        sig: PredSignature,
270        p: Arc<dyn LocyPredicate>,
271    ) -> Result<&mut Self, PluginError> {
272        self.require(&Capability::LocyPredicate)?;
273        self.validate_qname(&qname)?;
274        self.pending
275            .push(Box::new(NamedUniqueReg::<LocyPredicateSurface> {
276                q: qname,
277                sig,
278                provider: p,
279            }));
280        Ok(self)
281    }
282
283    /// Register a Locy generator predicate (table-valued, binds 1:N variables).
284    ///
285    /// # Errors
286    ///
287    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::LocyGenerator`] is absent.
288    pub fn locy_generator(
289        &mut self,
290        qname: QName,
291        sig: GenSignature,
292        p: Arc<dyn LocyGenerator>,
293    ) -> Result<&mut Self, PluginError> {
294        self.require(&Capability::LocyGenerator)?;
295        self.validate_qname(&qname)?;
296        self.pending
297            .push(Box::new(NamedUniqueReg::<LocyGeneratorSurface> {
298                q: qname,
299                sig,
300                provider: p,
301            }));
302        Ok(self)
303    }
304
305    /// Register an optimizer rule.
306    ///
307    /// # Errors
308    ///
309    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Operator`] is absent.
310    pub fn optimizer_rule(
311        &mut self,
312        r: Arc<dyn OptimizerRuleProvider>,
313    ) -> Result<&mut Self, PluginError> {
314        self.require(&Capability::Operator)?;
315        self.pending
316            .push(Box::new(AppendReg::<OptimizerRuleSurface> { provider: r }));
317        Ok(self)
318    }
319
320    /// Register an index kind.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Index`] is absent.
325    pub fn index_kind(
326        &mut self,
327        kind: IndexKind,
328        p: Arc<dyn IndexKindProvider>,
329    ) -> Result<&mut Self, PluginError> {
330        self.require(&Capability::Index)?;
331        self.pending
332            .push(Box::new(KeyedUniqueReg::<IndexKindSurface> {
333                key_override: Some(kind),
334                provider: p,
335            }));
336        Ok(self)
337    }
338
339    /// Register a per-label plugin storage (M5h.2).
340    ///
341    /// Native-schema label scans for `label` will be routed through
342    /// `storage` instead of the host's native backend.
343    ///
344    /// # Errors
345    ///
346    /// Returns [`PluginError::CapabilityRequired`] if
347    /// [`Capability::Storage`] is absent.
348    pub fn label_storage(
349        &mut self,
350        label: impl Into<SmolStr>,
351        storage: Arc<dyn crate::traits::storage::Storage>,
352    ) -> Result<&mut Self, PluginError> {
353        self.require(&Capability::Storage)?;
354        self.pending
355            .push(Box::new(KeyedUniqueReg::<LabelStorageSurface> {
356                key_override: Some(label.into()),
357                provider: storage,
358            }));
359        Ok(self)
360    }
361
362    /// Register a graph algorithm.
363    ///
364    /// # Errors
365    ///
366    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Algorithm`] is absent.
367    pub fn algorithm(
368        &mut self,
369        qname: QName,
370        p: Arc<dyn AlgorithmProvider>,
371    ) -> Result<&mut Self, PluginError> {
372        self.require(&Capability::Algorithm)?;
373        self.validate_qname(&qname)?;
374        // Slice-version negotiation (proposal §4.3 / D6): refuse at load time if
375        // the algorithm declares a capability slice/version the host does not
376        // implement, rather than trapping later on an unknown kernel op.
377        p.signature()
378            .check_slices(crate::traits::algorithm::HOST_CAPABILITY_SLICES)
379            .map_err(|e| PluginError::SliceUnavailable(e.message))?;
380        // Snapshot the effective caps so the stored entry can gate host
381        // graph access (e.g. `HostQuery`) at CALL time.
382        let effective_caps = self.effective_caps.clone();
383        self.pending
384            .push(Box::new(NamedUniqueReg::<AlgorithmSurface> {
385                q: qname,
386                sig: effective_caps,
387                provider: p,
388            }));
389        Ok(self)
390    }
391
392    /// Register a CRDT kind.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Crdt`] is absent.
397    pub fn crdt_kind(
398        &mut self,
399        kind: CrdtKind,
400        p: Arc<dyn CrdtKindProvider>,
401    ) -> Result<&mut Self, PluginError> {
402        self.require(&Capability::Crdt)?;
403        self.pending.push(Box::new(KeyedUniqueReg::<CrdtSurface> {
404            key_override: Some(kind),
405            provider: p,
406        }));
407        Ok(self)
408    }
409
410    /// Register a session-lifecycle hook.
411    ///
412    /// # Errors
413    ///
414    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Hook`] is absent.
415    pub fn hook(&mut self, h: Arc<dyn SessionHook>) -> Result<&mut Self, PluginError> {
416        self.require(&Capability::Hook)?;
417        self.pending
418            .push(Box::new(AppendReg::<HookSurface> { provider: h }));
419        Ok(self)
420    }
421
422    /// Register a logical type.
423    ///
424    /// # Errors
425    ///
426    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Type`] is absent.
427    pub fn logical_type(
428        &mut self,
429        t: Arc<dyn LogicalTypeProvider>,
430    ) -> Result<&mut Self, PluginError> {
431        self.require(&Capability::Type)?;
432        self.pending
433            .push(Box::new(KeyedUniqueReg::<LogicalTypeSurface> {
434                key_override: None,
435                provider: t,
436            }));
437        Ok(self)
438    }
439
440    /// Register an authentication provider.
441    ///
442    /// # Errors
443    ///
444    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Auth`] is absent.
445    pub fn auth_provider(&mut self, p: Arc<dyn AuthProvider>) -> Result<&mut Self, PluginError> {
446        self.require(&Capability::Auth)?;
447        self.pending
448            .push(Box::new(AppendReg::<AuthSurface> { provider: p }));
449        Ok(self)
450    }
451
452    /// Register an authorization policy.
453    ///
454    /// # Errors
455    ///
456    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Authz`] is absent.
457    pub fn authz_policy(&mut self, p: Arc<dyn AuthzPolicy>) -> Result<&mut Self, PluginError> {
458        self.require(&Capability::Authz)?;
459        self.pending
460            .push(Box::new(AppendReg::<AuthzSurface> { provider: p }));
461        Ok(self)
462    }
463
464    /// Register a fine-grained trigger.
465    ///
466    /// # Errors
467    ///
468    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Trigger`] is absent.
469    pub fn trigger(&mut self, t: Arc<dyn TriggerPlugin>) -> Result<&mut Self, PluginError> {
470        self.require(&Capability::Trigger)?;
471        self.pending
472            .push(Box::new(AppendReg::<TriggerSurface> { provider: t }));
473        Ok(self)
474    }
475
476    /// Register a collation.
477    ///
478    /// # Errors
479    ///
480    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Collation`] is absent.
481    pub fn collation(&mut self, c: Arc<dyn CollationProvider>) -> Result<&mut Self, PluginError> {
482        self.require(&Capability::Collation)?;
483        self.pending
484            .push(Box::new(KeyedUniqueReg::<CollationSurface> {
485                key_override: None,
486                provider: c,
487            }));
488        Ok(self)
489    }
490
491    /// Register a CDC output sink.
492    ///
493    /// # Errors
494    ///
495    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Cdc`] is absent.
496    pub fn cdc_output(&mut self, c: Arc<dyn CdcOutputProvider>) -> Result<&mut Self, PluginError> {
497        self.require(&Capability::Cdc)?;
498        self.pending.push(Box::new(KeyedUniqueReg::<CdcSurface> {
499            key_override: None,
500            provider: c,
501        }));
502        Ok(self)
503    }
504
505    /// Register a catalog provider.
506    ///
507    /// # Errors
508    ///
509    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Catalog`] is absent.
510    pub fn catalog(&mut self, c: Arc<dyn CatalogProvider>) -> Result<&mut Self, PluginError> {
511        self.require(&Capability::Catalog)?;
512        self.pending
513            .push(Box::new(KeyedUniqueReg::<CatalogSurface> {
514                key_override: None,
515                provider: c,
516            }));
517        Ok(self)
518    }
519
520    /// Register a replacement-scan provider.
521    ///
522    /// # Errors
523    ///
524    /// Returns [`PluginError::CapabilityRequired`] if [`Capability::Catalog`] is absent.
525    pub fn replacement_scan(
526        &mut self,
527        r: Arc<dyn ReplacementScanProvider>,
528    ) -> Result<&mut Self, PluginError> {
529        self.require(&Capability::Catalog)?;
530        self.pending
531            .push(Box::new(AppendReg::<ReplacementScanSurface> {
532                provider: r,
533            }));
534        Ok(self)
535    }
536
537    /// Register a background-job provider.
538    ///
539    /// # Errors
540    ///
541    /// Returns [`PluginError::CapabilityRequired`] if no `BackgroundJob`
542    /// capability variant is present in the effective set.
543    pub fn background_job(
544        &mut self,
545        j: Arc<dyn BackgroundJobProvider>,
546    ) -> Result<&mut Self, PluginError> {
547        self.require(&Capability::BackgroundJob { max_concurrent: 0 })?;
548        self.pending
549            .push(Box::new(AppendReg::<BackgroundJobSurface> { provider: j }));
550        Ok(self)
551    }
552
553    /// Commit batched registrations to the registry.
554    ///
555    /// Called by the host loader after the plugin's `register()` returns
556    /// successfully; failures during `register()` are rolled back by simply
557    /// dropping the registrar without committing.
558    ///
559    /// # Errors
560    ///
561    /// Returns [`PluginError::DuplicateRegistration`] if any pending qname
562    /// is already taken in the registry.
563    pub fn commit_to_registry(self) -> Result<(), PluginError> {
564        self.registry.apply_pending(&self.plugin_id, self.pending)
565    }
566
567    /// Returns the number of pending registrations.
568    ///
569    /// Exposed for diagnostics and integration tests that want to verify
570    /// a plugin's `register()` queued the expected number of items before
571    /// the registrar commits.
572    #[must_use]
573    pub fn pending_len(&self) -> usize {
574        self.pending.len()
575    }
576}