pack_component/
lib.rs

1#![cfg_attr(target_arch = "wasm32", no_std)]
2
3#[cfg(target_arch = "wasm32")]
4extern crate alloc;
5
6mod data;
7
8#[cfg(target_arch = "wasm32")]
9use alloc::{
10    format,
11    string::{String, ToString},
12    vec::Vec,
13};
14#[cfg(not(target_arch = "wasm32"))]
15use greentic_interfaces_host::bindings::exports::greentic::interfaces_pack::component_api::ProviderMeta;
16#[cfg(not(target_arch = "wasm32"))]
17const _: fn(ProviderMeta) = |_meta| {};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20#[cfg(not(target_arch = "wasm32"))]
21use std::vec::Vec;
22
23#[derive(Debug, Clone, Serialize)]
24pub struct FlowInfo {
25    pub id: String,
26    pub human_name: Option<String>,
27    pub description: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize)]
31pub struct SchemaDoc {
32    pub flow_id: String,
33    pub schema_json: serde_json::Value,
34}
35
36#[derive(Debug, Clone, Serialize)]
37pub struct PrepareResult {
38    pub status: String,
39    pub error: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct RunResult {
44    pub status: String,
45    pub output: Option<serde_json::Value>,
46    pub error: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize)]
50pub struct A2AItem {
51    pub title: String,
52    pub flow_id: String,
53}
54
55pub trait PackExport {
56    fn list_flows(&self) -> Vec<FlowInfo>;
57    fn get_flow_schema(&self, flow_id: &str) -> Option<SchemaDoc>;
58    fn prepare_flow(&self, flow_id: &str) -> PrepareResult;
59    fn run_flow(&self, flow_id: &str, input: serde_json::Value) -> RunResult;
60    fn a2a_search(&self, query: &str) -> Vec<A2AItem>;
61}
62
63/// Return the embedded pack manifest as CBOR bytes.
64pub fn manifest_cbor() -> &'static [u8] {
65    data::MANIFEST_CBOR
66}
67
68/// Decode the embedded pack manifest into a `serde_json::Value`.
69pub fn manifest_value() -> Value {
70    serde_cbor::from_slice(data::MANIFEST_CBOR)
71        .expect("generated manifest bytes should always be valid CBOR")
72}
73
74/// Decode the embedded manifest into a strongly-typed `T`.
75pub fn manifest_as<T>() -> T
76where
77    T: for<'de> Deserialize<'de>,
78{
79    serde_cbor::from_slice(data::MANIFEST_CBOR)
80        .expect("generated manifest matches the requested type")
81}
82
83/// Access the embedded flow sources as `(id, raw_ygtc)` tuples.
84pub fn flows() -> &'static [(&'static str, &'static str)] {
85    data::FLOWS
86}
87
88/// Access the embedded templates as `(logical_path, bytes)` tuples.
89pub fn templates() -> &'static [(&'static str, &'static [u8])] {
90    data::TEMPLATES
91}
92
93/// Lookup a template payload by logical path.
94pub fn template_by_path(path: &str) -> Option<&'static [u8]> {
95    data::TEMPLATES
96        .iter()
97        .find(|(logical, _)| *logical == path)
98        .map(|(_, bytes)| *bytes)
99}
100
101/// Component instance implementing the `greentic:pack-export` interface.
102#[derive(Debug, Default)]
103pub struct Component;
104
105impl PackExport for Component {
106    fn list_flows(&self) -> Vec<FlowInfo> {
107        flows()
108            .iter()
109            .map(|(id, _)| FlowInfo {
110                id: (*id).to_string(),
111                human_name: None,
112                description: None,
113            })
114            .collect()
115    }
116
117    fn get_flow_schema(&self, flow_id: &str) -> Option<SchemaDoc> {
118        flows()
119            .iter()
120            .find(|(id, _)| *id == flow_id)
121            .map(|(id, _)| SchemaDoc {
122                flow_id: (*id).to_string(),
123                schema_json: serde_json::json!({}),
124            })
125    }
126
127    fn prepare_flow(&self, flow_id: &str) -> PrepareResult {
128        if flows().iter().any(|(id, _)| *id == flow_id) {
129            PrepareResult {
130                status: "ok".into(),
131                error: None,
132            }
133        } else {
134            PrepareResult {
135                status: "error".into(),
136                error: Some(format!("unknown flow: {flow_id}")),
137            }
138        }
139    }
140
141    fn run_flow(&self, flow_id: &str, input: Value) -> RunResult {
142        if let Some((_, source)) = flows().iter().find(|(id, _)| *id == flow_id) {
143            RunResult {
144                status: "ok".into(),
145                output: Some(serde_json::json!({
146                    "flow_id": flow_id,
147                    "source": source,
148                    "input_echo": input,
149                })),
150                error: None,
151            }
152        } else {
153            RunResult {
154                status: "error".into(),
155                output: None,
156                error: Some(format!("unknown flow: {flow_id}")),
157            }
158        }
159    }
160
161    fn a2a_search(&self, _query: &str) -> Vec<A2AItem> {
162        Vec::new()
163    }
164}
165
166/// Convenience helper for host environments that want an owned component.
167pub fn component() -> Component {
168    Component
169}
170
171// Export simple C ABI shims for the stub interface so a Wasm harness can
172// exercise the component without native bindings.
173#[unsafe(no_mangle)]
174pub extern "C" fn greentic_pack_export__list_flows(json_buffer: *mut u8, len: usize) -> usize {
175    let component = Component;
176    let flows = component.list_flows();
177    write_json_response(&flows, json_buffer, len)
178}
179
180#[unsafe(no_mangle)]
181/// # Safety
182///
183/// The caller must ensure that `flow_id_ptr` points to `flow_id_len` bytes of
184/// valid UTF-8 and that `json_buffer` points to a writable region of at least
185/// `len` bytes when non-null.
186pub unsafe extern "C" fn greentic_pack_export__prepare_flow(
187    flow_id_ptr: *const u8,
188    flow_id_len: usize,
189    json_buffer: *mut u8,
190    len: usize,
191) -> usize {
192    let component = Component;
193    let flow_id = unsafe { slice_to_str(flow_id_ptr, flow_id_len) };
194    let result = component.prepare_flow(flow_id);
195    write_json_response(&result, json_buffer, len)
196}
197
198#[unsafe(no_mangle)]
199/// # Safety
200///
201/// The caller must ensure that `flow_id_ptr` points to `flow_id_len` bytes of
202/// valid UTF-8 and that `json_buffer` points to a writable region of at least
203/// `len` bytes when non-null.
204pub unsafe extern "C" fn greentic_pack_export__run_flow(
205    flow_id_ptr: *const u8,
206    flow_id_len: usize,
207    json_buffer: *mut u8,
208    len: usize,
209) -> usize {
210    let component = Component;
211    let flow_id = unsafe { slice_to_str(flow_id_ptr, flow_id_len) };
212    let result = component.run_flow(flow_id, serde_json::Value::Null);
213    write_json_response(&result, json_buffer, len)
214}
215
216#[unsafe(no_mangle)]
217pub extern "C" fn greentic_pack_export__a2a_search(json_buffer: *mut u8, len: usize) -> usize {
218    let component = Component;
219    let items = component.a2a_search("");
220    write_json_response(&items, json_buffer, len)
221}
222
223fn write_json_response<T: serde::Serialize>(value: &T, buffer: *mut u8, len: usize) -> usize {
224    let json = serde_json::to_vec(value).expect("serialisation succeeds");
225    if buffer.is_null() || len == 0 {
226        return json.len();
227    }
228
229    let copy_len = core::cmp::min(json.len(), len);
230    unsafe {
231        core::ptr::copy_nonoverlapping(json.as_ptr(), buffer, copy_len);
232    }
233    copy_len
234}
235
236unsafe fn slice_to_str<'a>(ptr: *const u8, len: usize) -> &'a str {
237    let bytes = unsafe { core::slice::from_raw_parts(ptr, len) };
238    core::str::from_utf8(bytes).expect("flow id is valid utf-8")
239}