harmont_cli/plugin/
registry.rs1#![allow(clippy::missing_errors_doc)]
14#![allow(clippy::needless_pass_by_value)]
15#![allow(clippy::collapsible_if)]
16
17use std::collections::{BTreeMap, HashSet};
18use std::path::PathBuf;
19use std::sync::Arc;
20
21use anyhow::{Context, Result};
22use hm_plugin_protocol::{Capability, PluginManifest};
23
24use super::host::LoadedPlugin;
25use super::host_fns::HOST_FN_NAMES;
26use super::manifest::{ManifestError, validate_standalone};
27use super::paths;
28use crate::error::HmError;
29
30#[derive(Debug, Default)]
31pub struct RegistryConfig {
32 pub auto_discover: bool,
35 pub extra_paths: Vec<PathBuf>,
38 pub embedded: Vec<(&'static str, &'static [u8])>,
41 pub pool_sizes: BTreeMap<String, usize>,
46}
47
48#[derive(Debug)]
49pub struct PluginRegistry {
50 plugins: Vec<Arc<LoadedPlugin>>,
51 pub subcommand_index: BTreeMap<String, usize>,
52 pub runner_index: BTreeMap<String, usize>,
53 pub output_formatter_index: BTreeMap<String, usize>,
54 pub default_runner: Option<usize>,
55}
56
57impl PluginRegistry {
58 pub fn load(config: RegistryConfig) -> Result<Self> {
59 let host_fns: HashSet<&str> = HOST_FN_NAMES.iter().copied().collect();
60 let mut plugins: Vec<Arc<LoadedPlugin>> = Vec::new();
61
62 let max_instances = config
69 .pool_sizes
70 .values()
71 .copied()
72 .max()
73 .unwrap_or(1)
74 .max(1);
75
76 for (name, bytes) in &config.embedded {
77 let p = LoadedPlugin::from_bytes(bytes, max_instances)
78 .with_context(|| format!("embedded plugin '{name}'"))?;
79 validate(&p.manifest, &host_fns)?;
80 plugins.push(Arc::new(p));
81 }
82
83 if config.auto_discover {
84 for dir in [paths::user_plugins_dir(), paths::project_plugins_dir()]
85 .into_iter()
86 .flatten()
87 {
88 if !dir.is_dir() {
89 continue;
90 }
91 let entries =
92 std::fs::read_dir(&dir).with_context(|| format!("read {}", dir.display()))?;
93 for ent in entries {
94 let Ok(ent) = ent else { continue };
95 let path = ent.path();
96 if path.extension().and_then(|s| s.to_str()) != Some("wasm") {
97 continue;
98 }
99 let p = LoadedPlugin::from_file(path.clone(), max_instances)
100 .with_context(|| format!("load {}", path.display()))?;
101 validate(&p.manifest, &host_fns)?;
102 plugins.push(Arc::new(p));
103 }
104 }
105 }
106
107 for path in &config.extra_paths {
108 let p = LoadedPlugin::from_file(path.clone(), max_instances)
109 .with_context(|| format!("load {}", path.display()))?;
110 validate(&p.manifest, &host_fns)?;
111 plugins.push(Arc::new(p));
112 }
113
114 let mut me = Self {
115 plugins,
116 subcommand_index: BTreeMap::new(),
117 runner_index: BTreeMap::new(),
118 output_formatter_index: BTreeMap::new(),
119 default_runner: None,
120 };
121 me.index_capabilities()?;
122 Ok(me)
123 }
124
125 fn index_capabilities(&mut self) -> Result<()> {
126 for (i, p) in self.plugins.iter().enumerate() {
127 for cap in &p.manifest.capabilities {
128 match cap {
129 Capability::Subcommand(s) => {
130 if let Some(other) = self.subcommand_index.insert(s.verb.clone(), i) {
131 return Err(HmError::PluginConflict {
132 verb: s.verb.clone(),
133 plugin_a: self.plugins[other].manifest.name.clone(),
134 plugin_b: p.manifest.name.clone(),
135 }
136 .into());
137 }
138 }
139 Capability::StepExecutor(s) => {
140 if let Some(other) = self.runner_index.insert(s.runner.clone(), i) {
141 return Err(HmError::PluginConflict {
142 verb: format!("runner:{}", s.runner),
143 plugin_a: self.plugins[other].manifest.name.clone(),
144 plugin_b: p.manifest.name.clone(),
145 }
146 .into());
147 }
148 if s.default {
149 if let Some(other) = self.default_runner.replace(i) {
150 return Err(HmError::PluginConflict {
151 verb: "default-runner".into(),
152 plugin_a: self.plugins[other].manifest.name.clone(),
153 plugin_b: p.manifest.name.clone(),
154 }
155 .into());
156 }
157 }
158 }
159 Capability::OutputFormatter(s) => {
160 if let Some(other) = self.output_formatter_index.insert(s.name.clone(), i) {
161 return Err(HmError::PluginConflict {
162 verb: format!("format:{}", s.name),
163 plugin_a: self.plugins[other].manifest.name.clone(),
164 plugin_b: p.manifest.name.clone(),
165 }
166 .into());
167 }
168 }
169 Capability::LifecycleHook(_) => {
170 }
172 }
173 }
174 }
175 Ok(())
176 }
177
178 pub fn manifests(&self) -> impl Iterator<Item = &PluginManifest> {
179 self.plugins.iter().map(|p| &p.manifest)
180 }
181
182 #[must_use]
187 pub fn get(&self, idx: usize) -> Option<Arc<LoadedPlugin>> {
188 self.plugins.get(idx).cloned()
189 }
190
191 #[must_use]
195 pub fn default_runner_name(&self) -> Option<&str> {
196 let idx = self.default_runner?;
197 self.runner_index
198 .iter()
199 .find_map(|(name, &i)| (i == idx).then_some(name.as_str()))
200 }
201}
202
203fn validate(m: &PluginManifest, host_fns: &HashSet<&str>) -> Result<()> {
204 validate_standalone(m, host_fns).map_err(|e| match e {
205 ManifestError::ApiVersion {
206 name,
207 found,
208 expected,
209 } => HmError::PluginManifest {
210 name,
211 expected_api: expected,
212 found_api: found,
213 }
214 .into(),
215 ManifestError::MissingHostFn { name, fn_name } => HmError::PluginMissingHostFn {
216 name,
217 fn_name,
218 min_hm_version: semver::Version::new(0, 0, 0),
219 }
220 .into(),
221 ManifestError::NoCapabilities { ref name }
222 | ManifestError::BadRunnerName { ref name, .. }
223 | ManifestError::DuplicateSubcommandVerb { ref name, .. } => HmError::PluginLoad {
224 name: name.clone(),
225 path: std::path::PathBuf::new(),
226 reason: e.to_string(),
227 doc_url: "https://harmont.dev/docs/plugins/manifest",
228 }
229 .into(),
230 })
231}