Skip to main content

component_dwbase/
lib.rs

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(&current_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}