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}