Skip to main content

greentic_ext_runtime/
runtime_dw_composer.rs

1//! `greentic:dw-composer/composer` interface dispatch.
2//!
3//! Composer extensions export `greentic:dw-composer/composer@0.1.0` or
4//! `@0.2.0` instead of the generic `greentic:extension-design/tools`
5//! interface.  This module provides type-safe invoke methods that resolve the
6//! composer interface directly from the component, bypassing the `invoke_tool`
7//! / `tools` path that raises boot warnings for every composer extension.
8//!
9//! The pattern mirrors [`crate::runtime_roles`] (see `list_roles` /
10//! `compile_role`): build a fresh `Store + Instance`, resolve the named
11//! interface export, obtain the typed function, call it.
12
13use crate::error::RuntimeError;
14use crate::loaded::ExtensionId;
15use crate::runtime::ExtensionRuntime;
16
17/// Version resolution order for the dw-composer interface — newest first.
18const COMPOSER_VERSIONS: &[&str] = &["0.2.0", "0.1.0"];
19/// Base interface name without the version suffix.
20const COMPOSER_IFACE_BASE: &str = "greentic:dw-composer/composer";
21
22impl ExtensionRuntime {
23    /// Call `greentic:dw-composer/composer::metadata` and return the JSON
24    /// descriptor string.
25    ///
26    /// The designer parses the returned JSON into its own `ComposerDescriptor`
27    /// type. No input argument is required — the function takes no parameters.
28    ///
29    /// # Errors
30    ///
31    /// Returns `RuntimeError::NotFound` when no extension is loaded at
32    /// `ext_id`, and `RuntimeError::Wasmtime` for any host-level failure
33    /// (missing interface, trap, type mismatch).
34    pub fn invoke_composer_metadata(&self, ext_id: &str) -> Result<String, RuntimeError> {
35        let loaded = self
36            .loaded()
37            .get(&ExtensionId(ext_id.to_string()))
38            .cloned()
39            .ok_or_else(|| RuntimeError::NotFound(ext_id.to_string()))?;
40
41        let (mut store, instance) = loaded
42            .build_store_and_instance(
43                self.engine(),
44                self.host_overrides().clone(),
45                &crate::host_ports::HostCallContext::default(),
46            )
47            .map_err(RuntimeError::Wasmtime)?;
48
49        let (iface_idx, iface_name, _version) = resolve_dw_composer_iface(&mut store, &instance)?;
50
51        let func_idx = instance
52            .get_export_index(&mut store, Some(&iface_idx), "metadata")
53            .ok_or_else(|| {
54                RuntimeError::Wasmtime(anyhow::anyhow!(
55                    "interface '{iface_name}' does not export 'metadata'"
56                ))
57            })?;
58
59        let func = instance
60            .get_typed_func::<(), (String,)>(&mut store, &func_idx)
61            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
62
63        let (result,) = func
64            .call(&mut store, ())
65            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
66
67        Ok(result)
68    }
69
70    /// Call `greentic:dw-composer/composer::compose(intent_json)` and return
71    /// the rendered manifest JSON on success or a canonical
72    /// [`crate::types::HostExtensionError`] on failure.
73    ///
74    /// `intent_json` is a JSON-serialised `ComposeIntent` whose schema is
75    /// defined by the specific composer extension. The `Ok` variant carries
76    /// the rendered manifest JSON; the `Err` variant carries a
77    /// `HostExtensionError` (v0.1.0 bare strings map to `Internal`).
78    ///
79    /// # Errors
80    ///
81    /// Returns `RuntimeError::NotFound` when no extension is loaded at
82    /// `ext_id`, and `RuntimeError::Wasmtime` for any host-level failure
83    /// (missing interface, trap, type mismatch). A business-logic failure
84    /// inside the composer is NOT a `RuntimeError` — it is the
85    /// `Err(HostExtensionError)` inside the returned
86    /// `Ok(Result<String, HostExtensionError>)`.
87    pub fn invoke_composer_compose(
88        &self,
89        ext_id: &str,
90        intent_json: &str,
91    ) -> Result<Result<String, crate::types::HostExtensionError>, RuntimeError> {
92        let loaded = self
93            .loaded()
94            .get(&ExtensionId(ext_id.to_string()))
95            .cloned()
96            .ok_or_else(|| RuntimeError::NotFound(ext_id.to_string()))?;
97
98        let (mut store, instance) = loaded
99            .build_store_and_instance(
100                self.engine(),
101                self.host_overrides().clone(),
102                &crate::host_ports::HostCallContext::default(),
103            )
104            .map_err(RuntimeError::Wasmtime)?;
105
106        let (iface_idx, iface_name, version) = resolve_dw_composer_iface(&mut store, &instance)?;
107
108        let func_idx = instance
109            .get_export_index(&mut store, Some(&iface_idx), "compose")
110            .ok_or_else(|| {
111                RuntimeError::Wasmtime(anyhow::anyhow!(
112                    "interface '{iface_name}' does not export 'compose'"
113                ))
114            })?;
115
116        let result: Result<String, crate::types::HostExtensionError> = if version == "0.2.0" {
117            use crate::host_bindings::dw_composer_v02::greentic::extension_base0_2_0::types::ExtensionError as E2;
118            let func = instance
119                .get_typed_func::<(String,), (Result<String, E2>,)>(&mut store, &func_idx)
120                .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
121            let (r,) = func
122                .call(&mut store, (intent_json.to_string(),))
123                .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
124            r.map_err(crate::ext_error::from_composer_v02)
125        } else {
126            let func = instance
127                .get_typed_func::<(String,), (Result<String, String>,)>(&mut store, &func_idx)
128                .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
129            let (r,) = func
130                .call(&mut store, (intent_json.to_string(),))
131                .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
132            r.map_err(crate::ext_error::from_composer_v01)
133        };
134
135        Ok(result)
136    }
137
138    /// Call `greentic:dw-composer/composer::validate(manifest_json)` and
139    /// return the JSON validation report string.
140    ///
141    /// The returned string is a JSON-serialised `ValidationReport` (exact
142    /// schema is defined by the composer extension — typically an array of
143    /// diagnostic objects or an object with a `valid` boolean field).
144    ///
145    /// # Errors
146    ///
147    /// Returns `RuntimeError::NotFound` when no extension is loaded at
148    /// `ext_id`, and `RuntimeError::Wasmtime` for host-level failures.
149    pub fn invoke_composer_validate(
150        &self,
151        ext_id: &str,
152        manifest_json: &str,
153    ) -> Result<String, RuntimeError> {
154        let loaded = self
155            .loaded()
156            .get(&ExtensionId(ext_id.to_string()))
157            .cloned()
158            .ok_or_else(|| RuntimeError::NotFound(ext_id.to_string()))?;
159
160        let (mut store, instance) = loaded
161            .build_store_and_instance(
162                self.engine(),
163                self.host_overrides().clone(),
164                &crate::host_ports::HostCallContext::default(),
165            )
166            .map_err(RuntimeError::Wasmtime)?;
167
168        let (iface_idx, iface_name, _version) = resolve_dw_composer_iface(&mut store, &instance)?;
169
170        let func_idx = instance
171            .get_export_index(&mut store, Some(&iface_idx), "validate")
172            .ok_or_else(|| {
173                RuntimeError::Wasmtime(anyhow::anyhow!(
174                    "interface '{iface_name}' does not export 'validate'"
175                ))
176            })?;
177
178        let func = instance
179            .get_typed_func::<(String,), (String,)>(&mut store, &func_idx)
180            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
181
182        let (result,) = func
183            .call(&mut store, (manifest_json.to_string(),))
184            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
185
186        Ok(result)
187    }
188
189    /// Call `greentic:dw-composer/composer::templates` and return the JSON
190    /// array string of `ComposerTemplate` objects.
191    ///
192    /// # Errors
193    ///
194    /// Returns `RuntimeError::NotFound` when no extension is loaded at
195    /// `ext_id`, and `RuntimeError::Wasmtime` for host-level failures.
196    pub fn invoke_composer_templates(&self, ext_id: &str) -> Result<String, RuntimeError> {
197        let loaded = self
198            .loaded()
199            .get(&ExtensionId(ext_id.to_string()))
200            .cloned()
201            .ok_or_else(|| RuntimeError::NotFound(ext_id.to_string()))?;
202
203        let (mut store, instance) = loaded
204            .build_store_and_instance(
205                self.engine(),
206                self.host_overrides().clone(),
207                &crate::host_ports::HostCallContext::default(),
208            )
209            .map_err(RuntimeError::Wasmtime)?;
210
211        let (iface_idx, iface_name, _version) = resolve_dw_composer_iface(&mut store, &instance)?;
212
213        let func_idx = instance
214            .get_export_index(&mut store, Some(&iface_idx), "templates")
215            .ok_or_else(|| {
216                RuntimeError::Wasmtime(anyhow::anyhow!(
217                    "interface '{iface_name}' does not export 'templates'"
218                ))
219            })?;
220
221        let func = instance
222            .get_typed_func::<(), (String,)>(&mut store, &func_idx)
223            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
224
225        let (result,) = func
226            .call(&mut store, ())
227            .map_err(|e| RuntimeError::Wasmtime(e.into()))?;
228
229        Ok(result)
230    }
231}
232
233/// Resolve `greentic:dw-composer/composer` from a wasmtime component instance,
234/// trying `@0.2.0` first then falling back to `@0.1.0`.
235///
236/// Returns the `ComponentExportIndex` for the interface, the resolved
237/// fully-qualified name (for use in subsequent error messages), and the bare
238/// matched version string (`"0.2.0"` or `"0.1.0"`).
239///
240/// # Errors
241///
242/// Returns `RuntimeError::Wasmtime` with a descriptive message when the
243/// component does not export the expected interface at any supported version.
244fn resolve_dw_composer_iface(
245    store: &mut wasmtime::Store<crate::host_state::HostState>,
246    instance: &wasmtime::component::Instance,
247) -> Result<
248    (
249        wasmtime::component::ComponentExportIndex,
250        String,
251        &'static str,
252    ),
253    RuntimeError,
254> {
255    crate::runtime::resolve_iface_versions(store, instance, COMPOSER_IFACE_BASE, COMPOSER_VERSIONS)
256}