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