1use crate::contract::{Compute, ComputeError, TransformBackend};
17use crate::manifest::{parse_and_validate, ImplKind, ManifestError, TransformManifest};
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeMap;
20use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22
23pub struct RawUnit {
25 pub root: String,
26 pub dir: PathBuf,
27 pub toml: String,
28}
29
30pub trait Root: Send + Sync {
34 fn name(&self) -> &str;
36 fn discover(&self) -> Result<Vec<RawUnit>, RegistryError>;
38}
39
40pub struct DirRoot {
43 name: String,
44 path: PathBuf,
45}
46
47impl DirRoot {
48 pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
49 Self {
50 name: name.into(),
51 path: path.into(),
52 }
53 }
54}
55
56impl Root for DirRoot {
57 fn name(&self) -> &str {
58 &self.name
59 }
60
61 fn discover(&self) -> Result<Vec<RawUnit>, RegistryError> {
62 if !self.path.exists() {
63 return Ok(Vec::new());
64 }
65 let mut out = Vec::new();
66 let mut entries: Vec<PathBuf> = std::fs::read_dir(&self.path)
67 .map_err(|e| RegistryError::Io {
68 path: self.path.clone(),
69 msg: e.to_string(),
70 })?
71 .filter_map(|e| e.ok().map(|e| e.path()))
72 .filter(|p| p.is_dir())
73 .collect();
74 entries.sort(); for dir in entries {
76 let manifest_path = dir.join("transform.toml");
77 if !manifest_path.exists() {
78 continue; }
80 let toml = std::fs::read_to_string(&manifest_path).map_err(|e| RegistryError::Io {
81 path: manifest_path.clone(),
82 msg: e.to_string(),
83 })?;
84 out.push(RawUnit {
85 root: self.name.clone(),
86 dir,
87 toml,
88 });
89 }
90 Ok(out)
91 }
92}
93
94pub struct MemoryRoot {
96 name: String,
97 units: Vec<(PathBuf, String)>,
98}
99
100impl MemoryRoot {
101 pub fn new(name: impl Into<String>) -> Self {
102 Self {
103 name: name.into(),
104 units: Vec::new(),
105 }
106 }
107 pub fn unit(mut self, dir: impl Into<PathBuf>, toml: impl Into<String>) -> Self {
109 self.units.push((dir.into(), toml.into()));
110 self
111 }
112}
113
114impl Root for MemoryRoot {
115 fn name(&self) -> &str {
116 &self.name
117 }
118 fn discover(&self) -> Result<Vec<RawUnit>, RegistryError> {
119 Ok(self
120 .units
121 .iter()
122 .map(|(dir, toml)| RawUnit {
123 root: self.name.clone(),
124 dir: dir.clone(),
125 toml: toml.clone(),
126 })
127 .collect())
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RegisteredTransform {
134 pub manifest: TransformManifest,
135 pub dir: PathBuf,
136 pub root: String,
138}
139
140impl RegisteredTransform {
141 pub fn key(&self) -> String {
143 format!("{}@{}", self.manifest.id, self.manifest.version)
144 }
145}
146
147#[derive(Debug, thiserror::Error)]
148pub enum RegistryError {
149 #[error("I/O error at {path}: {msg}")]
150 Io { path: PathBuf, msg: String },
151 #[error("invalid transform in root `{root}` at {dir}: {source}")]
152 Manifest {
153 root: String,
154 dir: PathBuf,
155 source: Box<ManifestError>,
156 },
157 #[error("duplicate transform `{key}` within root `{root}` ({dir})")]
158 DuplicateInRoot { key: String, root: String, dir: PathBuf },
159 #[error("registry index contract version `{found}` != `{expected}`")]
160 IndexVersion { found: String, expected: String },
161}
162
163#[derive(Default)]
167pub struct RegistryBuilder {
168 roots: Vec<Box<dyn Root>>,
169}
170
171impl RegistryBuilder {
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn root(mut self, r: impl Root + 'static) -> Self {
178 self.roots.push(Box::new(r));
179 self
180 }
181
182 pub fn build(self) -> Result<Registry, RegistryError> {
183 let mut by_key: BTreeMap<String, RegisteredTransform> = BTreeMap::new();
184 for root in &self.roots {
185 let mut seen_in_root: BTreeMap<String, PathBuf> = BTreeMap::new();
186 for raw in root.discover()? {
187 let manifest = parse_and_validate(&raw.toml).map_err(|source| RegistryError::Manifest {
188 root: raw.root.clone(),
189 dir: raw.dir.clone(),
190 source: Box::new(source),
191 })?;
192 let reg = RegisteredTransform {
193 manifest,
194 dir: raw.dir.clone(),
195 root: raw.root.clone(),
196 };
197 let key = reg.key();
198 if let Some(prev) = seen_in_root.insert(key.clone(), raw.dir.clone()) {
199 let _ = prev;
200 return Err(RegistryError::DuplicateInRoot {
201 key,
202 root: raw.root,
203 dir: raw.dir,
204 });
205 }
206 by_key.insert(key, reg); }
208 }
209 Ok(Registry { by_key })
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct Registry {
216 by_key: BTreeMap<String, RegisteredTransform>,
217}
218
219impl Registry {
220 pub fn builder() -> RegistryBuilder {
221 RegistryBuilder::new()
222 }
223
224 pub fn get(&self, id: &str, version: &str) -> Option<&RegisteredTransform> {
226 self.by_key.get(&format!("{id}@{version}"))
227 }
228
229 pub fn latest(&self, id: &str) -> Option<&RegisteredTransform> {
231 self.by_key.values().filter(|r| r.manifest.id == id).max_by(|a, b| {
232 let va = semver::Version::parse(&a.manifest.version).ok();
233 let vb = semver::Version::parse(&b.manifest.version).ok();
234 va.cmp(&vb)
235 })
236 }
237
238 pub fn resolve(&self, selector: &str) -> Option<&RegisteredTransform> {
240 match selector.split_once('@') {
241 Some((id, version)) => self.get(id, version),
242 None => self.latest(selector),
243 }
244 }
245
246 pub fn iter(&self) -> impl Iterator<Item = &RegisteredTransform> {
247 self.by_key.values()
248 }
249
250 pub fn len(&self) -> usize {
251 self.by_key.len()
252 }
253
254 pub fn is_empty(&self) -> bool {
255 self.by_key.is_empty()
256 }
257
258 pub fn to_index(&self) -> RegistryIndex {
261 RegistryIndex {
262 contract_version: crate::CONTRACT_VERSION.to_string(),
263 transforms: self.by_key.values().cloned().collect(),
264 }
265 }
266
267 pub fn from_index(index: RegistryIndex) -> Result<Self, RegistryError> {
270 if index.contract_version != crate::CONTRACT_VERSION {
271 return Err(RegistryError::IndexVersion {
272 found: index.contract_version,
273 expected: crate::CONTRACT_VERSION.to_string(),
274 });
275 }
276 let by_key = index.transforms.into_iter().map(|r| (r.key(), r)).collect();
277 Ok(Registry { by_key })
278 }
279
280 pub fn load(&self, selector: &str, backends: &Backends) -> Result<Box<dyn Compute>, ComputeError> {
283 let reg = self.resolve(selector).ok_or_else(|| ComputeError::Load {
284 id: selector.to_string(),
285 msg: "no such transform in registry".into(),
286 })?;
287 let backend = backends.get(reg.manifest.impl_kind).ok_or(ComputeError::NoBackend {
288 kind: reg.manifest.impl_kind,
289 })?;
290 backend.load(®.manifest, ®.dir)
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct RegistryIndex {
297 #[serde(rename = "contractVersion")]
298 pub contract_version: String,
299 pub transforms: Vec<RegisteredTransform>,
300}
301
302#[derive(Default)]
305pub struct Backends {
306 map: HashMap<ImplKind, Box<dyn TransformBackend>>,
307}
308
309impl Backends {
310 pub fn new() -> Self {
311 Self::default()
312 }
313
314 pub fn register(mut self, backend: impl TransformBackend + 'static) -> Self {
315 self.map.insert(backend.kind(), Box::new(backend));
316 self
317 }
318
319 pub fn get(&self, kind: ImplKind) -> Option<&dyn TransformBackend> {
320 self.map.get(&kind).map(|b| b.as_ref())
321 }
322
323 pub fn kinds(&self) -> impl Iterator<Item = &ImplKind> {
324 self.map.keys()
325 }
326}
327
328pub fn standard_registry(provided: impl AsRef<Path>, business_bundles: &[PathBuf]) -> Result<Registry, RegistryError> {
332 let mut b = Registry::builder().root(DirRoot::new("provided", provided.as_ref().to_path_buf()));
333 for bundle in business_bundles {
334 let name = bundle
335 .file_name()
336 .map(|s| s.to_string_lossy().to_string())
337 .unwrap_or_else(|| "business".into());
338 b = b.root(DirRoot::new(name, bundle.join("transforms")));
339 }
340 b.build()
341}