greentic_dev/
component_resolver.rs

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