1#[cfg(target_arch = "wasm32")]
2use std::collections::BTreeMap;
3
4#[cfg(target_arch = "wasm32")]
5use greentic_interfaces_guest::component_v0_6::{component_i18n, component_qa, node};
6#[cfg(target_arch = "wasm32")]
7use greentic_types::cbor::canonical;
8#[cfg(target_arch = "wasm32")]
9use greentic_types::schemas::common::schema_ir::{AdditionalProperties, SchemaIr};
10#[cfg(target_arch = "wasm32")]
11use greentic_types::schemas::component::v0_6_0::{ComponentInfo, I18nText};
12
13pub mod i18n;
14pub mod i18n_bundle;
15pub mod qa;
16
17const COMPONENT_NAME: &str = "dwbase";
18#[cfg(target_arch = "wasm32")]
19const COMPONENT_ORG: &str = "ai.greentic";
20#[cfg(target_arch = "wasm32")]
21const COMPONENT_VERSION: &str = env!("CARGO_PKG_VERSION");
22#[cfg(target_arch = "wasm32")]
23const DEFAULT_OPERATION: &str = "dwbase.configure";
24
25#[cfg(target_arch = "wasm32")]
26#[used]
27#[unsafe(link_section = ".greentic.wasi")]
28static WASI_TARGET_MARKER: [u8; 13] = *b"wasm32-wasip2";
29
30#[cfg(target_arch = "wasm32")]
31struct Component;
32
33#[cfg(target_arch = "wasm32")]
34impl node::Guest for Component {
35 fn describe() -> node::ComponentDescriptor {
36 let input_schema_cbor = input_schema_cbor();
37 let output_schema_cbor = output_schema_cbor();
38 let mut ops = vec![
39 node::Op {
40 name: DEFAULT_OPERATION.to_string(),
41 summary: Some(
42 "Validate and normalize DWBase capability-pack configuration.".to_string(),
43 ),
44 input: node::IoSchema {
45 schema: node::SchemaSource::InlineCbor(input_schema_cbor.clone()),
46 content_type: "application/cbor".to_string(),
47 schema_version: None,
48 },
49 output: node::IoSchema {
50 schema: node::SchemaSource::InlineCbor(output_schema_cbor.clone()),
51 content_type: "application/cbor".to_string(),
52 schema_version: None,
53 },
54 examples: Vec::new(),
55 },
56 node::Op {
57 name: "dwbase.requirements".to_string(),
58 summary: Some(
59 "Report DWBase capability and public ingress requirements.".to_string(),
60 ),
61 input: node::IoSchema {
62 schema: node::SchemaSource::InlineCbor(input_schema_cbor.clone()),
63 content_type: "application/cbor".to_string(),
64 schema_version: None,
65 },
66 output: node::IoSchema {
67 schema: node::SchemaSource::InlineCbor(output_schema_cbor.clone()),
68 content_type: "application/cbor".to_string(),
69 schema_version: None,
70 },
71 examples: Vec::new(),
72 },
73 node::Op {
74 name: "dwbase.echo".to_string(),
75 summary: Some("Echo a DWBase request envelope for smoke testing.".to_string()),
76 input: node::IoSchema {
77 schema: node::SchemaSource::InlineCbor(input_schema_cbor.clone()),
78 content_type: "application/cbor".to_string(),
79 schema_version: None,
80 },
81 output: node::IoSchema {
82 schema: node::SchemaSource::InlineCbor(output_schema_cbor.clone()),
83 content_type: "application/cbor".to_string(),
84 schema_version: None,
85 },
86 examples: Vec::new(),
87 },
88 ];
89 ops.extend(vec![
90 node::Op {
91 name: "qa-spec".to_string(),
92 summary: Some("Return QA spec for requested mode.".to_string()),
93 input: node::IoSchema {
94 schema: node::SchemaSource::InlineCbor(input_schema_cbor.clone()),
95 content_type: "application/cbor".to_string(),
96 schema_version: None,
97 },
98 output: node::IoSchema {
99 schema: node::SchemaSource::InlineCbor(output_schema_cbor.clone()),
100 content_type: "application/cbor".to_string(),
101 schema_version: None,
102 },
103 examples: Vec::new(),
104 },
105 node::Op {
106 name: "apply-answers".to_string(),
107 summary: Some("Apply QA answers and return a config patch.".to_string()),
108 input: node::IoSchema {
109 schema: node::SchemaSource::InlineCbor(input_schema_cbor.clone()),
110 content_type: "application/cbor".to_string(),
111 schema_version: None,
112 },
113 output: node::IoSchema {
114 schema: node::SchemaSource::InlineCbor(output_schema_cbor.clone()),
115 content_type: "application/cbor".to_string(),
116 schema_version: None,
117 },
118 examples: Vec::new(),
119 },
120 node::Op {
121 name: "i18n-keys".to_string(),
122 summary: Some("Return i18n keys referenced by QA/setup.".to_string()),
123 input: node::IoSchema {
124 schema: node::SchemaSource::InlineCbor(input_schema_cbor.clone()),
125 content_type: "application/cbor".to_string(),
126 schema_version: None,
127 },
128 output: node::IoSchema {
129 schema: node::SchemaSource::InlineCbor(output_schema_cbor),
130 content_type: "application/cbor".to_string(),
131 schema_version: None,
132 },
133 examples: Vec::new(),
134 },
135 ]);
136 node::ComponentDescriptor {
137 name: COMPONENT_NAME.to_string(),
138 version: COMPONENT_VERSION.to_string(),
139 summary: Some(
140 "DWBase Greentic capability provider with ingress-aware QA and configuration."
141 .to_string(),
142 ),
143 capabilities: Vec::new(),
144 ops,
145 schemas: Vec::new(),
146 setup: None,
147 }
148 }
149
150 fn invoke(
151 operation: String,
152 envelope: node::InvocationEnvelope,
153 ) -> Result<node::InvocationResult, node::NodeError> {
154 let output = run_component_cbor(&operation, envelope.payload_cbor);
155 Ok(node::InvocationResult {
156 ok: true,
157 output_cbor: output,
158 output_metadata_cbor: None,
159 })
160 }
161}
162
163#[cfg(target_arch = "wasm32")]
164impl component_qa::Guest for Component {
165 fn qa_spec(mode: component_qa::QaMode) -> Vec<u8> {
166 let mode = match mode {
167 component_qa::QaMode::Default => qa::NormalizedMode::Setup,
168 component_qa::QaMode::Setup => qa::NormalizedMode::Setup,
169 component_qa::QaMode::Update => qa::NormalizedMode::Update,
170 component_qa::QaMode::Remove => qa::NormalizedMode::Remove,
171 };
172 encode_cbor(&qa::qa_spec_json(mode))
173 }
174
175 fn apply_answers(
176 mode: component_qa::QaMode,
177 current_config: Vec<u8>,
178 answers: Vec<u8>,
179 ) -> Vec<u8> {
180 let mode = match mode {
181 component_qa::QaMode::Default => qa::NormalizedMode::Setup,
182 component_qa::QaMode::Setup => qa::NormalizedMode::Setup,
183 component_qa::QaMode::Update => qa::NormalizedMode::Update,
184 component_qa::QaMode::Remove => qa::NormalizedMode::Remove,
185 };
186
187 let mut payload = parse_payload(¤t_config);
188 let answer_value = parse_payload(&answers);
189 if let Some(map) = payload.as_object_mut() {
190 if let Some(answer_map) = answer_value.as_object() {
191 for (key, value) in answer_map {
192 map.insert(key.clone(), value.clone());
193 }
194 }
195 } else {
196 payload = answer_value;
197 }
198 encode_cbor(&qa::apply_answers(mode, &payload))
199 }
200}
201
202#[cfg(target_arch = "wasm32")]
203impl component_i18n::Guest for Component {
204 fn i18n_keys() -> Vec<String> {
205 qa::i18n_keys()
206 }
207}
208
209#[cfg(target_arch = "wasm32")]
210greentic_interfaces_guest::export_component_v060!(
211 Component,
212 component_qa: Component,
213 component_i18n: Component,
214);
215
216pub fn handle_message(operation: &str, input: &str) -> String {
217 format!("{COMPONENT_NAME}::{operation} => {}", input.trim())
218}
219
220#[cfg(target_arch = "wasm32")]
221fn encode_cbor<T: serde::Serialize>(value: &T) -> Vec<u8> {
222 canonical::to_canonical_cbor_allow_floats(value).expect("encode cbor")
223}
224
225#[cfg(target_arch = "wasm32")]
226fn parse_payload(input: &[u8]) -> serde_json::Value {
227 if let Ok(value) = canonical::from_cbor(input) {
228 return value;
229 }
230 serde_json::from_slice(input).unwrap_or_else(|_| serde_json::json!({}))
231}
232
233#[cfg(target_arch = "wasm32")]
234fn normalized_mode(payload: &serde_json::Value) -> qa::NormalizedMode {
235 let mode = payload
236 .get("mode")
237 .and_then(|v| v.as_str())
238 .or_else(|| payload.get("operation").and_then(|v| v.as_str()))
239 .unwrap_or("setup");
240 qa::normalize_mode(mode).unwrap_or(qa::NormalizedMode::Setup)
241}
242
243#[cfg(target_arch = "wasm32")]
244fn input_schema() -> SchemaIr {
245 SchemaIr::Object {
246 properties: BTreeMap::from([(
247 "input".to_string(),
248 SchemaIr::String {
249 min_len: Some(0),
250 max_len: None,
251 regex: None,
252 format: None,
253 },
254 )]),
255 required: vec!["input".to_string()],
256 additional: AdditionalProperties::Forbid,
257 }
258}
259
260#[cfg(target_arch = "wasm32")]
261fn output_schema() -> SchemaIr {
262 SchemaIr::Object {
263 properties: BTreeMap::from([(
264 "message".to_string(),
265 SchemaIr::String {
266 min_len: Some(0),
267 max_len: None,
268 regex: None,
269 format: None,
270 },
271 )]),
272 required: vec!["message".to_string()],
273 additional: AdditionalProperties::Forbid,
274 }
275}
276
277#[cfg(target_arch = "wasm32")]
278#[allow(dead_code)]
279fn config_schema() -> SchemaIr {
280 SchemaIr::Object {
281 properties: BTreeMap::new(),
282 required: Vec::new(),
283 additional: AdditionalProperties::Forbid,
284 }
285}
286
287#[cfg(target_arch = "wasm32")]
288#[allow(dead_code)]
289fn component_info() -> ComponentInfo {
290 ComponentInfo {
291 id: format!("{COMPONENT_ORG}.{COMPONENT_NAME}"),
292 version: COMPONENT_VERSION.to_string(),
293 role: "tool".to_string(),
294 display_name: Some(I18nText::new(
295 "component.display_name",
296 Some(COMPONENT_NAME.to_string()),
297 )),
298 }
299}
300
301#[cfg(target_arch = "wasm32")]
302fn input_schema_cbor() -> Vec<u8> {
303 encode_cbor(&input_schema())
304}
305
306#[cfg(target_arch = "wasm32")]
307fn output_schema_cbor() -> Vec<u8> {
308 encode_cbor(&output_schema())
309}
310
311#[cfg(target_arch = "wasm32")]
312fn run_component_cbor(operation: &str, input: Vec<u8>) -> Vec<u8> {
313 let value = parse_payload(&input);
314 let output = match operation {
315 "dwbase.configure" => qa::configure(&value),
316 "dwbase.requirements" => qa::requirements_json(),
317 "qa-spec" => {
318 let mode = normalized_mode(&value);
319 qa::qa_spec_json(mode)
320 }
321 "apply-answers" => {
322 let mode = normalized_mode(&value);
323 qa::apply_answers(mode, &value)
324 }
325 "i18n-keys" => serde_json::Value::Array(
326 qa::i18n_keys()
327 .into_iter()
328 .map(serde_json::Value::String)
329 .collect(),
330 ),
331 "dwbase.echo" => {
332 let op_name = value
333 .get("operation")
334 .and_then(|v| v.as_str())
335 .unwrap_or(operation);
336 let input_text = value
337 .get("input")
338 .and_then(|v| v.as_str())
339 .map(ToOwned::to_owned)
340 .unwrap_or_else(|| value.to_string());
341 serde_json::json!({
342 "message": handle_message(op_name, &input_text)
343 })
344 }
345 _ => qa::configure(&value),
346 };
347 encode_cbor(&output)
348}