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}