Skip to main content

cordis_loader/
registry.rs

1//! Static plugin registry — the Rust replacement for upstream's dynamic
2//! `import(name)`.
3
4use cordis::utils::BoxFuture;
5use cordis::{Config, Context, Inject, Plugin, PluginHandle, PluginOutput, Result};
6use cordis_group::Group;
7use cordis_include::IMPORT_NAME;
8use cordis_include::resolver::unknown_plugin;
9use std::collections::HashMap;
10#[cfg(feature = "dynamic")]
11use std::path::PathBuf;
12use std::sync::Arc;
13
14/// A factory producing fresh plugin handles.
15type Factory = Arc<dyn Fn() -> PluginHandle + Send + Sync>;
16
17/// Name-to-factory plugin registry, populated at build or startup time.
18///
19/// Resolving the same name twice yields distinct [`PluginHandle`]s (and
20/// thus distinct [`cordis::PluginKey`] identities), mirroring one plugin
21/// instance per entry. The registry pre-registers [`cordis_group`]'s
22/// `group` marker.
23///
24/// With the `dynamic` feature, a
25/// [`DynamicPluginResolver`](crate::dynamic::DynamicPluginResolver) can be
26/// attached as a fallback: names missing from the registry are then looked
27/// up as dynamic libraries in its directories.
28#[derive(Clone, Default)]
29pub struct PluginRegistry {
30    factories: HashMap<String, Factory>,
31    #[cfg(feature = "dynamic")]
32    dynamic: Option<crate::dynamic::DynamicPluginResolver>,
33}
34
35impl PluginRegistry {
36    /// An empty registry with the `group` and `import` builtins registered.
37    pub fn new() -> Self {
38        let mut registry = Self::default();
39        registry.register("group", Group::handle);
40        registry.register(IMPORT_NAME, || PluginHandle::new(Import));
41        registry
42    }
43
44    /// Register a handle factory under `name`.
45    pub fn register<F>(&mut self, name: impl Into<String>, factory: F)
46    where
47        F: Fn() -> PluginHandle + Send + Sync + 'static,
48    {
49        self.factories.insert(name.into(), Arc::new(factory));
50    }
51
52    /// Register one plugin instance under its own name; every resolve then
53    /// wraps a shared clone of it in a fresh handle.
54    pub fn register_plugin<P: Plugin>(&mut self, plugin: P) {
55        let name = plugin.name().to_owned();
56        let shared = Arc::new(plugin);
57        self.register(name, move || {
58            PluginHandle::new(SharedPlugin(shared.clone()))
59        });
60    }
61
62    /// Registered names, unordered.
63    pub fn names(&self) -> impl Iterator<Item = &str> {
64        self.factories.keys().map(String::as_str)
65    }
66
67    /// Attach `resolver` as the fallback for unknown names (dynamic
68    /// feature).
69    #[cfg(feature = "dynamic")]
70    pub fn set_dynamic(&mut self, resolver: crate::dynamic::DynamicPluginResolver) {
71        self.dynamic = Some(resolver);
72    }
73
74    /// Builder form of [`PluginRegistry::set_dynamic`]: search `dirs` for
75    /// dynamic-library plugins when a name is not statically registered.
76    #[cfg(feature = "dynamic")]
77    pub fn with_dynamic_dirs<I, D>(mut self, dirs: I) -> Self
78    where
79        I: IntoIterator<Item = D>,
80        D: Into<PathBuf>,
81    {
82        self.set_dynamic(crate::dynamic::DynamicPluginResolver::new(dirs));
83        self
84    }
85
86    /// The attached dynamic resolver, if any (dynamic feature).
87    #[cfg(feature = "dynamic")]
88    pub fn dynamic(&self) -> Option<&crate::dynamic::DynamicPluginResolver> {
89        self.dynamic.as_ref()
90    }
91}
92
93impl cordis_include::PluginResolver for PluginRegistry {
94    fn resolve(&self, name: &str) -> Result<PluginHandle> {
95        match self.factories.get(name) {
96            Some(factory) => Ok(factory()),
97            #[cfg(feature = "dynamic")]
98            None => match &self.dynamic {
99                Some(resolver) => resolver.resolve(name),
100                None => Err(unknown_plugin(name)),
101            },
102            #[cfg(not(feature = "dynamic"))]
103            None => Err(unknown_plugin(name)),
104        }
105    }
106}
107
108/// Nesting marker for import entries, whose children the loader mounts
109/// from the referenced file at compose time.
110pub(crate) struct Import;
111
112impl Plugin for Import {
113    fn name(&self) -> &str {
114        IMPORT_NAME
115    }
116
117    fn apply(&self, _ctx: Context, _config: Config) -> BoxFuture<Result<PluginOutput>> {
118        Box::pin(async { Ok(PluginOutput::default()) })
119    }
120}
121
122/// Wraps one shared plugin instance so clones produce distinct handles.
123struct SharedPlugin<P>(Arc<P>);
124
125impl<P: Plugin> Plugin for SharedPlugin<P> {
126    fn name(&self) -> &str {
127        self.0.name()
128    }
129
130    fn inject(&self) -> &Inject {
131        self.0.inject()
132    }
133
134    fn validate_config(&self, config: Config) -> Result<Config> {
135        self.0.validate_config(config)
136    }
137
138    fn apply(&self, ctx: Context, config: Config) -> BoxFuture<Result<PluginOutput>> {
139        self.0.apply(ctx, config)
140    }
141}
142
143/// Wraps a resolved plugin, merging an entry's `inject` declaration into the
144/// plugin's own dependencies.
145///
146/// With this in place, the core fiber machinery already reconciles entries
147/// when an injected service goes away or comes back — no loader-side batch
148/// refresh needed.
149pub(crate) struct WithInject {
150    handle: PluginHandle,
151    inject: Inject,
152}
153
154impl WithInject {
155    /// Wrap `handle`, merging an entry's `inject` declaration into the
156    /// plugin's own dependencies — the plugin's names first, then the
157    /// entry's extras, deduplicated. An empty entry list adds nothing and
158    /// keeps the bare handle.
159    pub fn wrap(handle: PluginHandle, inject: Vec<String>) -> PluginHandle {
160        if inject.is_empty() {
161            return handle;
162        }
163        let mut names: Vec<String> = handle
164            .plugin()
165            .inject()
166            .names()
167            .map(ToString::to_string)
168            .collect();
169        for name in inject {
170            if !names.contains(&name) {
171                names.push(name);
172            }
173        }
174        PluginHandle::new(WithInject {
175            handle,
176            inject: Inject::new(names),
177        })
178    }
179}
180
181impl Plugin for WithInject {
182    fn name(&self) -> &str {
183        self.handle.name()
184    }
185
186    fn inject(&self) -> &Inject {
187        &self.inject
188    }
189
190    fn validate_config(&self, config: Config) -> Result<Config> {
191        self.handle.plugin().validate_config(config)
192    }
193
194    fn apply(&self, ctx: Context, config: Config) -> BoxFuture<Result<PluginOutput>> {
195        self.handle.plugin().apply(ctx, config)
196    }
197}