use std::collections::BTreeMap;
use crate::ext::describe::{BundleRecipeContribution, Descriptor, Execution};
use crate::ext::errors::ExtensionError;
use crate::ext::loader::DiscoveredExtension;
#[derive(Debug, Clone)]
pub struct RecipeEntry {
pub extension_id: String,
pub extension_version: String,
pub recipe: BundleRecipeContribution,
pub execution: Execution,
pub descriptor_root: std::path::PathBuf,
}
#[derive(Debug, Clone, Default)]
pub struct ExtensionRegistry {
entries: BTreeMap<String, RecipeEntry>,
}
impl ExtensionRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register_discovered(
&mut self,
discovered: Vec<DiscoveredExtension>,
) -> Result<(), ExtensionError> {
for d in discovered {
self.add_descriptor(d.descriptor, d.root)?;
}
Ok(())
}
pub fn add_descriptor(
&mut self,
descriptor: Descriptor,
root: std::path::PathBuf,
) -> Result<(), ExtensionError> {
let ext_id = descriptor.metadata.id.clone();
let ext_ver = descriptor.metadata.version.clone();
for recipe in descriptor.contributions.recipes {
let key = format!("{ext_id}/{}", recipe.id);
if self.entries.contains_key(&key) {
return Err(ExtensionError::Conflict(key));
}
self.entries.insert(
key,
RecipeEntry {
extension_id: ext_id.clone(),
extension_version: ext_ver.clone(),
recipe,
execution: descriptor.execution.clone(),
descriptor_root: root.clone(),
},
);
}
Ok(())
}
pub fn resolve(
&self,
extension_id: &str,
recipe_id: &str,
) -> Result<&RecipeEntry, ExtensionError> {
let key = format!("{extension_id}/{recipe_id}");
self.entries
.get(&key)
.ok_or_else(|| ExtensionError::RecipeNotFound {
ext: extension_id.into(),
recipe: recipe_id.into(),
})
}
pub fn list(&self) -> impl Iterator<Item = &RecipeEntry> {
self.entries.values()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ext::describe::Descriptor;
use std::path::PathBuf;
fn d(id: &str, recipe_id: &str) -> (Descriptor, PathBuf) {
let raw = format!(
r#"{{
"apiVersion": "greentic.ai/v1",
"kind": "BundleExtension",
"metadata": {{ "id": "{id}", "name": "x", "version": "0.1.0" }},
"runtime": {{ "component": "extension.wasm" }},
"execution": {{ "kind": "wasm" }},
"contributions": {{
"recipes": [
{{ "id": "{recipe_id}", "displayName": "x", "description": "x",
"configSchema": "s.json" }}
]
}}
}}"#
);
(Descriptor::from_json(&raw).unwrap(), PathBuf::from("/tmp"))
}
#[test]
fn resolve_unknown_returns_error() {
let r = ExtensionRegistry::new();
let err = r.resolve("x", "y").unwrap_err();
assert!(matches!(err, ExtensionError::RecipeNotFound { .. }));
}
#[test]
fn register_and_resolve() {
let mut r = ExtensionRegistry::new();
let (desc, root) = d("greentic.bundle-standard", "standard");
r.add_descriptor(desc, root).unwrap();
let entry = r.resolve("greentic.bundle-standard", "standard").unwrap();
assert_eq!(entry.recipe.id, "standard");
}
#[test]
fn conflict_same_ext_same_recipe() {
let mut r = ExtensionRegistry::new();
let (desc1, root1) = d("greentic.bundle-standard", "standard");
let (desc2, root2) = d("greentic.bundle-standard", "standard");
r.add_descriptor(desc1, root1).unwrap();
let err = r.add_descriptor(desc2, root2).unwrap_err();
assert!(matches!(err, ExtensionError::Conflict(_)));
}
#[test]
fn no_conflict_different_ext_same_recipe_id() {
let mut r = ExtensionRegistry::new();
let (desc1, root1) = d("greentic.bundle-a", "standard");
let (desc2, root2) = d("greentic.bundle-b", "standard");
r.add_descriptor(desc1, root1).unwrap();
r.add_descriptor(desc2, root2).unwrap();
assert_eq!(r.list().count(), 2);
}
}