greentic_dev/
component_resolver.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::{Context, Result, anyhow, bail};
6use greentic_component::describe::{DescribePayload, DescribeVersion};
7use greentic_component::lifecycle::Lifecycle;
8use greentic_component::manifest::ComponentManifest;
9use greentic_component::prepare::PreparedComponent;
10use greentic_component::prepare_component;
11use greentic_flow::flow_bundle::NodeRef;
12use jsonschema::{Draft, Validator};
13use semver::{Version, VersionReq};
14use serde::Serialize;
15use serde_json::Value as JsonValue;
16
17#[derive(Debug, Clone)]
18pub struct ResolvedComponent {
19    pub name: String,
20    pub version: Version,
21    pub wasm_path: PathBuf,
22    #[allow(dead_code)]
23    pub manifest_path: PathBuf,
24    pub schema_json: Option<String>,
25    pub manifest_json: Option<String>,
26    pub capabilities_json: Option<JsonValue>,
27    #[allow(dead_code)]
28    pub limits_json: Option<JsonValue>,
29    pub world: String,
30    pub wasm_hash: String,
31    #[allow(dead_code)]
32    describe: DescribePayload,
33}
34
35#[derive(Debug, Clone)]
36pub struct ResolvedNode {
37    pub node_id: String,
38    pub component: Arc<ResolvedComponent>,
39    pub pointer: String,
40    pub config: JsonValue,
41}
42
43#[derive(Debug, Clone)]
44pub struct NodeSchemaError {
45    pub node_id: String,
46    pub component: String,
47    pub pointer: String,
48    pub message: String,
49}
50
51#[derive(Debug, Clone, Hash, PartialEq, Eq)]
52struct ComponentCacheKey {
53    name: String,
54    version: Version,
55}
56
57impl ComponentCacheKey {
58    fn new(name: impl Into<String>, version: &Version) -> Self {
59        Self {
60            name: name.into(),
61            version: version.clone(),
62        }
63    }
64}
65
66pub struct ComponentResolver {
67    component_dir: Option<PathBuf>,
68    cache: HashMap<ComponentCacheKey, Arc<ResolvedComponent>>,
69    schema_cache: HashMap<String, Arc<CachedSchema>>,
70}
71
72struct CachedSchema(Validator);
73
74impl ComponentResolver {
75    pub fn new(component_dir: Option<PathBuf>) -> Self {
76        Self {
77            component_dir,
78            cache: HashMap::new(),
79            schema_cache: HashMap::new(),
80        }
81    }
82
83    pub fn resolve_node(&mut self, node: &NodeRef, flow_doc: &JsonValue) -> Result<ResolvedNode> {
84        let component_key = &node.component;
85        let pointer = format!("/nodes/{}/{}", node.node_id, component_key.name);
86        let config = extract_node_payload(flow_doc, &node.node_id, &component_key.name)
87            .with_context(|| {
88                format!(
89                    "failed to extract payload for node `{}` ({})",
90                    node.node_id, component_key.name
91                )
92            })?;
93
94        let version_req = parse_version_req(&component_key.version_req).with_context(|| {
95            format!(
96                "node `{}` has invalid semver requirement `{}`",
97                node.node_id, component_key.version_req
98            )
99        })?;
100
101        let component = self
102            .load_component(&component_key.name, &version_req)
103            .with_context(|| {
104                format!(
105                    "node `{}`: failed to prepare component `{}`",
106                    node.node_id, component_key.name
107                )
108            })?;
109
110        Ok(ResolvedNode {
111            node_id: node.node_id.clone(),
112            component,
113            pointer,
114            config,
115        })
116    }
117
118    pub fn validate_node(&mut self, node: &ResolvedNode) -> Result<Vec<NodeSchemaError>> {
119        let Some(schema_json) = &node.component.schema_json else {
120            return Ok(Vec::new());
121        };
122
123        let validator = self.compile_schema(schema_json)?;
124        let mut issues = Vec::new();
125        if let Err(error) = validator.0.validate(&node.config) {
126            for error in std::iter::once(error).chain(validator.0.iter_errors(&node.config)) {
127                let suffix = error.instance_path().to_string();
128                let pointer = if suffix.is_empty() || suffix == "/" {
129                    node.pointer.clone()
130                } else {
131                    format!("{}{}", node.pointer, suffix)
132                };
133                issues.push(NodeSchemaError {
134                    node_id: node.node_id.clone(),
135                    component: node.component.name.clone(),
136                    pointer,
137                    message: error.to_string(),
138                });
139            }
140        }
141        Ok(issues)
142    }
143
144    fn compile_schema(&mut self, schema_json: &str) -> Result<Arc<CachedSchema>> {
145        if let Some(existing) = self.schema_cache.get(schema_json) {
146            return Ok(existing.clone());
147        }
148
149        let schema_value: JsonValue =
150            serde_json::from_str(schema_json).context("invalid schema JSON")?;
151        let compiled = jsonschema::options()
152            .with_draft(Draft::Draft7)
153            .build(&schema_value)
154            .map_err(|error| anyhow!("failed to compile schema JSON: {error}"))?;
155        let entry = Arc::new(CachedSchema(compiled));
156        self.schema_cache
157            .insert(schema_json.to_string(), entry.clone());
158        Ok(entry)
159    }
160
161    fn load_component(
162        &mut self,
163        name: &str,
164        version_req: &VersionReq,
165    ) -> Result<Arc<ResolvedComponent>> {
166        let target = component_target(name, self.component_dir.as_deref());
167        let target_display = match &target {
168            ComponentTarget::Direct(id) => id.clone(),
169            ComponentTarget::Path(path) => path.display().to_string(),
170        };
171
172        let prepared = prepare_component(target.as_ref()).with_context(|| {
173            format!(
174                "resolver looked for `{name}` via `{target_display}` but prepare_component failed"
175            )
176        })?;
177
178        if !version_req.matches(&prepared.manifest.version) {
179            bail!(
180                "component `{name}` version `{}` does not satisfy requirement `{version_req}`",
181                prepared.manifest.version
182            );
183        }
184
185        let key = ComponentCacheKey::new(name, &prepared.manifest.version);
186        if let Some(existing) = self.cache.get(&key) {
187            return Ok(existing.clone());
188        }
189
190        let resolved = Arc::new(to_resolved_component(prepared)?);
191        self.cache.insert(key, resolved.clone());
192        Ok(resolved)
193    }
194}
195
196enum ComponentTarget {
197    Direct(String),
198    Path(PathBuf),
199}
200
201impl ComponentTarget {
202    fn as_ref(&self) -> &str {
203        match self {
204            ComponentTarget::Direct(id) => id,
205            ComponentTarget::Path(path) => path.to_str().expect("component path utf-8"),
206        }
207    }
208}
209
210fn component_target(name: &str, root: Option<&Path>) -> ComponentTarget {
211    if let Some(dir) = root {
212        let candidate = dir.join(name);
213        return ComponentTarget::Path(candidate);
214    }
215    ComponentTarget::Direct(name.to_string())
216}
217
218fn parse_version_req(input: &str) -> Result<VersionReq> {
219    if input.trim().is_empty() {
220        VersionReq::parse("*").map_err(Into::into)
221    } else {
222        VersionReq::parse(input).map_err(Into::into)
223    }
224}
225
226fn to_resolved_component(prepared: PreparedComponent) -> Result<ResolvedComponent> {
227    let manifest_json =
228        serde_json::to_string(&prepared.manifest).context("failed to serialize manifest")?;
229    let capabilities_json = serde_json::to_value(&prepared.manifest.capabilities)
230        .context("failed to serialize capabilities")?;
231    let limits_json = prepared
232        .manifest
233        .limits
234        .as_ref()
235        .map(|limits| serde_json::to_value(limits).expect("limits serialize"));
236    let schema_json = select_schema(&prepared.describe);
237
238    Ok(ResolvedComponent {
239        name: prepared.manifest.id.as_str().to_string(),
240        version: prepared.manifest.version.clone(),
241        wasm_path: prepared.wasm_path.clone(),
242        manifest_path: prepared.manifest_path.clone(),
243        schema_json,
244        manifest_json: Some(manifest_json),
245        capabilities_json: Some(capabilities_json),
246        limits_json,
247        world: prepared.manifest.world.as_str().to_string(),
248        wasm_hash: prepared.wasm_hash.clone(),
249        describe: prepared.describe,
250    })
251}
252
253fn extract_node_payload(
254    document: &JsonValue,
255    node_id: &str,
256    component_name: &str,
257) -> Result<JsonValue> {
258    let nodes = document
259        .get("nodes")
260        .and_then(|value| value.as_object())
261        .context("flow document missing `nodes` object")?;
262
263    let node_entry = nodes
264        .get(node_id)
265        .and_then(|value| value.as_object())
266        .context(format!("flow document missing node `{node_id}`"))?;
267
268    let payload = node_entry.get(component_name).cloned().context(format!(
269        "node `{node_id}` missing component payload `{component_name}`"
270    ))?;
271
272    Ok(payload)
273}
274
275fn select_schema(describe: &DescribePayload) -> Option<String> {
276    choose_latest_version(&describe.versions)
277        .map(|entry| serde_json::to_string(&entry.schema).expect("describe schema serializes"))
278}
279
280fn choose_latest_version(versions: &[DescribeVersion]) -> Option<DescribeVersion> {
281    let mut sorted = versions.to_vec();
282    sorted.sort_by(|a, b| b.version.cmp(&a.version));
283    sorted.into_iter().next()
284}
285
286#[allow(dead_code)]
287#[derive(Serialize)]
288struct PreparedComponentView<'a> {
289    manifest: &'a ComponentManifest,
290    manifest_path: String,
291    wasm_path: String,
292    wasm_hash: &'a str,
293    world_ok: bool,
294    hash_verified: bool,
295    describe: &'a DescribePayload,
296    lifecycle: &'a Lifecycle,
297}
298
299#[allow(dead_code)]
300pub fn inspect(target: &str, compact_json: bool) -> Result<()> {
301    let prepared = prepare_component(target)
302        .with_context(|| format!("failed to prepare component `{target}`"))?;
303    let view = PreparedComponentView {
304        manifest: &prepared.manifest,
305        manifest_path: prepared.manifest_path.display().to_string(),
306        wasm_path: prepared.wasm_path.display().to_string(),
307        wasm_hash: &prepared.wasm_hash,
308        world_ok: prepared.world_ok,
309        hash_verified: prepared.hash_verified,
310        describe: &prepared.describe,
311        lifecycle: &prepared.lifecycle,
312    };
313
314    if compact_json {
315        println!("{}", serde_json::to_string(&view)?);
316    } else {
317        println!("{}", serde_json::to_string_pretty(&view)?);
318    }
319    Ok(())
320}