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([("enabled".to_string(), SchemaIr::Bool)]),
202 required: Vec::new(),
203 additional: AdditionalProperties::Forbid,
204 }
205}
206
207#[cfg(target_arch = "wasm32")]
208fn component_info() -> ComponentInfo {
209 ComponentInfo {
210 id: format!("{COMPONENT_ORG}.{COMPONENT_NAME}"),
211 version: COMPONENT_VERSION.to_string(),
212 role: "tool".to_string(),
213 display_name: Some(I18nText::new(
214 "component.display_name",
215 Some(COMPONENT_NAME.to_string()),
216 )),
217 }
218}
219
220#[cfg(target_arch = "wasm32")]
221fn component_describe() -> ComponentDescribe {
222 let input = input_schema();
223 let output = output_schema();
224 let config = config_schema();
225 let hash = schema_hash(&input, &output, &config).unwrap_or_default();
226
227 ComponentDescribe {
228 info: component_info(),
229 provided_capabilities: Vec::new(),
230 required_capabilities: Vec::new(),
231 metadata: BTreeMap::new(),
232 operations: vec![ComponentOperation {
233 id: "run".to_string(),
234 display_name: Some(I18nText::new("operation.run", Some("Run".to_string()))),
235 input: ComponentRunInput {
236 schema: input.clone(),
237 },
238 output: ComponentRunOutput {
239 schema: output.clone(),
240 },
241 defaults: BTreeMap::new(),
242 redactions: Vec::new(),
243 constraints: BTreeMap::new(),
244 schema_hash: hash,
245 }],
246 config_schema: config,
247 }
248}
249
250#[cfg(target_arch = "wasm32")]
251fn run_component_cbor(input: Vec<u8>, state: Vec<u8>) -> component_runtime::RunResult {
252 let value = parse_payload(&input);
253 let operation = value
254 .get("operation")
255 .and_then(|v| v.as_str())
256 .unwrap_or("handle_message");
257 let output = match operation {
258 "qa-spec" => {
259 let mode = value
260 .get("mode")
261 .and_then(|v| v.as_str())
262 .and_then(qa::normalize_mode)
263 .unwrap_or(qa::NormalizedMode::Setup);
264 qa::qa_spec_json(mode, &value)
265 }
266 "apply-answers" => {
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::apply_answers(mode, &value)
273 }
274 "i18n-keys" => serde_json::Value::Array(
275 qa::i18n_keys()
276 .into_iter()
277 .map(serde_json::Value::String)
278 .collect(),
279 ),
280 _ => {
281 let input_text = value
282 .get("input")
283 .and_then(|v| v.as_str())
284 .map(ToOwned::to_owned)
285 .unwrap_or_else(|| value.to_string());
286 serde_json::json!({
287 "message": handle_message(operation, &input_text)
288 })
289 }
290 };
291
292 component_runtime::RunResult {
293 output: encode_cbor(&output),
294 new_state: state,
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn describe_payload_is_json() {
304 let payload = describe_payload();
305 let json: serde_json::Value = serde_json::from_str(&payload).expect("valid json");
306 assert_eq!(json["component"]["name"], "component-qa");
307 }
308
309 #[test]
310 fn handle_message_round_trips() {
311 let body = handle_message("handle", "demo");
312 assert!(body.contains("demo"));
313 }
314}