1#[cfg(target_arch = "wasm32")]
2use std::collections::BTreeMap;
3
4#[cfg(target_arch = "wasm32")]
5use greentic_types::cbor::canonical;
6#[cfg(target_arch = "wasm32")]
7use greentic_types::schemas::common::schema_ir::{AdditionalProperties, SchemaIr};
8#[cfg(target_arch = "wasm32")]
9use greentic_types::schemas::component::v0_6_0::{
10 ComponentDescribe, ComponentInfo, ComponentOperation, ComponentRunInput, ComponentRunOutput,
11 I18nText, schema_hash,
12};
13
14#[cfg(target_arch = "wasm32")]
15mod bindings;
16#[cfg(target_arch = "wasm32")]
17use bindings::exports::greentic::component::{
18 component_descriptor, component_i18n, component_qa, component_runtime, component_schema,
19};
20
21pub mod i18n;
22pub mod i18n_bundle;
23pub mod qa;
24pub use qa::{
25 apply_store, describe, get_answer_schema, get_example_answers, next, next_with_ctx,
26 render_card, render_json_ui, render_text, submit_all, submit_patch, validate_answers,
27};
28
29const COMPONENT_NAME: &str = "component-qa";
30const COMPONENT_ORG: &str = "ai.greentic";
31const COMPONENT_VERSION: &str = env!("CARGO_PKG_VERSION");
32
33#[cfg(target_arch = "wasm32")]
34#[used]
35#[unsafe(link_section = ".greentic.wasi")]
36static WASI_TARGET_MARKER: [u8; 13] = *b"wasm32-wasip2";
37
38#[cfg(target_arch = "wasm32")]
39struct Component;
40
41#[cfg(target_arch = "wasm32")]
42impl component_descriptor::Guest for Component {
43 fn get_component_info() -> Vec<u8> {
44 encode_cbor(&component_info())
45 }
46
47 fn describe() -> Vec<u8> {
48 encode_cbor(&component_describe())
49 }
50}
51
52#[cfg(target_arch = "wasm32")]
53impl component_schema::Guest for Component {
54 fn input_schema() -> Vec<u8> {
55 encode_cbor(&input_schema())
56 }
57
58 fn output_schema() -> Vec<u8> {
59 encode_cbor(&output_schema())
60 }
61
62 fn config_schema() -> Vec<u8> {
63 encode_cbor(&config_schema())
64 }
65}
66
67#[cfg(target_arch = "wasm32")]
68impl component_runtime::Guest for Component {
69 fn run(input: Vec<u8>, state: Vec<u8>) -> component_runtime::RunResult {
70 run_component_cbor(input, state)
71 }
72}
73
74#[cfg(target_arch = "wasm32")]
75impl component_qa::Guest for Component {
76 fn qa_spec(mode: component_qa::QaMode) -> Vec<u8> {
77 let normalized = qa_mode_to_normalized(mode);
78 let mut spec = qa::qa_spec_json(normalized, &serde_json::json!({}));
79 if matches!(mode, component_qa::QaMode::Default)
80 && let Some(spec_obj) = spec.as_object_mut()
81 {
82 spec_obj.insert(
83 "mode".to_string(),
84 serde_json::Value::String("default".to_string()),
85 );
86 }
87 encode_cbor(&spec)
88 }
89
90 fn apply_answers(
91 mode: component_qa::QaMode,
92 current_config: Vec<u8>,
93 answers: Vec<u8>,
94 ) -> Vec<u8> {
95 let normalized = qa_mode_to_normalized(mode);
96 let payload = serde_json::json!({
97 "mode": normalized.as_str(),
98 "current_config": parse_payload(¤t_config),
99 "answers": parse_payload(&answers)
100 });
101 let result = qa::apply_answers(normalized, &payload);
102 let config = result
103 .get("config")
104 .cloned()
105 .or_else(|| payload.get("current_config").cloned())
106 .unwrap_or_else(|| serde_json::json!({}));
107 encode_cbor(&config)
108 }
109}
110
111#[cfg(target_arch = "wasm32")]
112impl component_i18n::Guest for Component {
113 fn i18n_keys() -> Vec<String> {
114 qa::i18n_keys()
115 }
116}
117
118#[cfg(target_arch = "wasm32")]
119bindings::export!(Component with_types_in bindings);
120
121pub fn describe_payload() -> String {
122 serde_json::json!({
123 "component": {
124 "name": COMPONENT_NAME,
125 "org": COMPONENT_ORG,
126 "version": COMPONENT_VERSION,
127 "world": "greentic:component/component@0.6.0",
128 "schemas": {
129 "component": "schemas/component.schema.json",
130 "input": "schemas/io/input.schema.json",
131 "output": "schemas/io/output.schema.json"
132 }
133 }
134 })
135 .to_string()
136}
137
138pub fn handle_message(operation: &str, input: &str) -> String {
139 format!("{COMPONENT_NAME}::{operation} => {}", input.trim())
140}
141
142#[cfg(target_arch = "wasm32")]
143fn encode_cbor<T: serde::Serialize>(value: &T) -> Vec<u8> {
144 canonical::to_canonical_cbor_allow_floats(value).expect("encode cbor")
145}
146
147#[cfg(target_arch = "wasm32")]
148fn parse_payload(input: &[u8]) -> serde_json::Value {
149 if let Ok(value) = canonical::from_cbor(input) {
150 return value;
151 }
152 serde_json::from_slice(input).unwrap_or_else(|_| serde_json::json!({}))
153}
154
155#[cfg(target_arch = "wasm32")]
156fn qa_mode_to_normalized(mode: component_qa::QaMode) -> qa::NormalizedMode {
157 match mode {
158 component_qa::QaMode::Default | component_qa::QaMode::Setup => qa::NormalizedMode::Setup,
159 component_qa::QaMode::Update => qa::NormalizedMode::Update,
160 component_qa::QaMode::Remove => qa::NormalizedMode::Remove,
161 }
162}
163
164#[cfg(target_arch = "wasm32")]
165fn input_schema() -> SchemaIr {
166 SchemaIr::Object {
167 properties: BTreeMap::from([(
168 "operation".to_string(),
169 SchemaIr::String {
170 min_len: Some(0),
171 max_len: None,
172 regex: None,
173 format: None,
174 },
175 )]),
176 required: Vec::new(),
177 additional: AdditionalProperties::Allow,
178 }
179}
180
181#[cfg(target_arch = "wasm32")]
182fn output_schema() -> SchemaIr {
183 SchemaIr::Object {
184 properties: BTreeMap::from([(
185 "message".to_string(),
186 SchemaIr::String {
187 min_len: Some(0),
188 max_len: None,
189 regex: None,
190 format: None,
191 },
192 )]),
193 required: Vec::new(),
194 additional: AdditionalProperties::Allow,
195 }
196}
197
198#[cfg(target_arch = "wasm32")]
199fn config_schema() -> SchemaIr {
200 SchemaIr::Object {
201 properties: BTreeMap::from([(
202 "qa_form_asset_path".to_string(),
203 SchemaIr::String {
204 min_len: Some(1),
205 max_len: None,
206 regex: None,
207 format: None,
208 },
209 )]),
210 required: vec!["qa_form_asset_path".to_string()],
211 additional: AdditionalProperties::Forbid,
212 }
213}
214
215#[cfg(target_arch = "wasm32")]
216fn component_info() -> ComponentInfo {
217 ComponentInfo {
218 id: format!("{COMPONENT_ORG}.{COMPONENT_NAME}"),
219 version: COMPONENT_VERSION.to_string(),
220 role: "tool".to_string(),
221 display_name: Some(I18nText::new(
222 "component.display_name",
223 Some(COMPONENT_NAME.to_string()),
224 )),
225 }
226}
227
228#[cfg(target_arch = "wasm32")]
229fn component_describe() -> ComponentDescribe {
230 let input = input_schema();
231 let output = output_schema();
232 let config = config_schema();
233 let hash = schema_hash(&input, &output, &config).unwrap_or_default();
234
235 ComponentDescribe {
236 info: component_info(),
237 provided_capabilities: Vec::new(),
238 required_capabilities: Vec::new(),
239 metadata: BTreeMap::new(),
240 operations: vec![ComponentOperation {
241 id: "run".to_string(),
242 display_name: Some(I18nText::new("operation.run", Some("Run".to_string()))),
243 input: ComponentRunInput {
244 schema: input.clone(),
245 },
246 output: ComponentRunOutput {
247 schema: output.clone(),
248 },
249 defaults: BTreeMap::new(),
250 redactions: Vec::new(),
251 constraints: BTreeMap::new(),
252 schema_hash: hash,
253 }],
254 config_schema: config,
255 }
256}
257
258#[cfg(target_arch = "wasm32")]
259fn run_component_cbor(input: Vec<u8>, state: Vec<u8>) -> component_runtime::RunResult {
260 let value = parse_payload(&input);
261 let operation = value
262 .get("operation")
263 .and_then(|v| v.as_str())
264 .unwrap_or("handle_message");
265 let output = match operation {
266 "qa-spec" => {
267 let mode = value
268 .get("mode")
269 .and_then(|v| v.as_str())
270 .and_then(qa::normalize_mode)
271 .unwrap_or(qa::NormalizedMode::Setup);
272 qa::qa_spec_json(mode, &value)
273 }
274 "apply-answers" => {
275 let mode = value
276 .get("mode")
277 .and_then(|v| v.as_str())
278 .and_then(qa::normalize_mode)
279 .unwrap_or(qa::NormalizedMode::Setup);
280 qa::apply_answers(mode, &value)
281 }
282 "i18n-keys" => serde_json::Value::Array(
283 qa::i18n_keys()
284 .into_iter()
285 .map(serde_json::Value::String)
286 .collect(),
287 ),
288 _ => {
289 let input_text = value
290 .get("input")
291 .and_then(|v| v.as_str())
292 .map(ToOwned::to_owned)
293 .unwrap_or_else(|| value.to_string());
294 serde_json::json!({
295 "message": handle_message(operation, &input_text)
296 })
297 }
298 };
299
300 component_runtime::RunResult {
301 output: encode_cbor(&output),
302 new_state: state,
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn describe_payload_is_json() {
312 let payload = describe_payload();
313 let json: serde_json::Value = serde_json::from_str(&payload).expect("valid json");
314 assert_eq!(json["component"]["name"], "component-qa");
315 }
316
317 #[test]
318 fn handle_message_round_trips() {
319 let body = handle_message("handle", "demo");
320 assert!(body.contains("demo"));
321 }
322}