Skip to main content

greentic_ext_runtime/
capability.rs

1use std::collections::HashMap;
2
3use greentic_extension_sdk_contract::{CapabilityId, CapabilityRef, ExtensionKind};
4use semver::{Version, VersionReq};
5
6#[derive(Debug, Clone)]
7pub struct OfferedBinding {
8    pub extension_id: String,
9    pub cap_id: CapabilityId,
10    pub version: Version,
11    pub kind: ExtensionKind,
12    pub export_path: String,
13}
14
15#[derive(Debug, Clone, Default)]
16pub struct ResolutionPlan {
17    pub consumer: String,
18    pub resolved: HashMap<CapabilityId, OfferedBinding>,
19    pub unresolved: Vec<CapabilityRef>,
20}
21
22#[derive(Debug, Default)]
23pub struct CapabilityRegistry {
24    offerings: HashMap<CapabilityId, Vec<OfferedBinding>>,
25}
26
27impl CapabilityRegistry {
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    pub fn add_offering(&mut self, offering: OfferedBinding) {
34        self.offerings
35            .entry(offering.cap_id.clone())
36            .or_default()
37            .push(offering);
38    }
39
40    #[must_use]
41    pub fn resolve(&self, consumer: &str, required: &[CapabilityRef]) -> ResolutionPlan {
42        let mut resolved = HashMap::new();
43        let mut unresolved = Vec::new();
44        for req in required {
45            let vr = VersionReq::parse(&req.version).unwrap_or(VersionReq::STAR);
46            let best = self
47                .offerings
48                .get(&req.id)
49                .and_then(|offers| {
50                    offers
51                        .iter()
52                        .filter(|o| vr.matches(&o.version))
53                        .max_by(|a, b| a.version.cmp(&b.version))
54                })
55                .cloned();
56            match best {
57                Some(o) => {
58                    resolved.insert(req.id.clone(), o);
59                }
60                None => unresolved.push(req.clone()),
61            }
62        }
63        ResolutionPlan {
64            consumer: consumer.to_string(),
65            resolved,
66            unresolved,
67        }
68    }
69
70    pub fn offerings(&self) -> impl Iterator<Item = &OfferedBinding> {
71        self.offerings.values().flat_map(|v| v.iter())
72    }
73
74    /// Returns extension IDs that participate in a dependency cycle.
75    /// Empty vec if acyclic.
76    #[must_use]
77    pub fn detect_cycle(&self, extensions: &[(String, Vec<CapabilityRef>)]) -> Vec<String> {
78        let ext_map: HashMap<&str, &Vec<CapabilityRef>> = extensions
79            .iter()
80            .map(|(id, reqs)| (id.as_str(), reqs))
81            .collect();
82
83        let mut in_cycle = Vec::new();
84        for (id, _) in extensions {
85            let mut visited = std::collections::HashSet::new();
86            if self.dfs_has_cycle(id, &ext_map, &mut visited) {
87                in_cycle.push(id.clone());
88            }
89        }
90        in_cycle
91    }
92
93    fn dfs_has_cycle(
94        &self,
95        ext_id: &str,
96        ext_map: &HashMap<&str, &Vec<CapabilityRef>>,
97        visited: &mut std::collections::HashSet<String>,
98    ) -> bool {
99        if !visited.insert(ext_id.to_string()) {
100            return true;
101        }
102        let Some(reqs) = ext_map.get(ext_id) else {
103            visited.remove(ext_id);
104            return false;
105        };
106        for req in *reqs {
107            let vr = VersionReq::parse(&req.version).unwrap_or(VersionReq::STAR);
108            let Some(offers) = self.offerings.get(&req.id) else {
109                continue;
110            };
111            for o in offers.iter().filter(|o| vr.matches(&o.version)) {
112                if self.dfs_has_cycle(&o.extension_id, ext_map, visited) {
113                    return true;
114                }
115            }
116        }
117        visited.remove(ext_id);
118        false
119    }
120}