1use anyhow::Result;
2
3pub mod v0_4 {
4 wasmtime::component::bindgen!({
5 inline: r#"
6 package greentic:component@0.4.0;
7
8 interface control {
9 should-cancel: func() -> bool;
10 yield-now: func();
11 }
12
13 interface node {
14 type json = string;
15
16 record tenant-ctx {
17 tenant: string,
18 team: option<string>,
19 user: option<string>,
20 trace-id: option<string>,
21 correlation-id: option<string>,
22 deadline-unix-ms: option<u64>,
23 attempt: u32,
24 idempotency-key: option<string>,
25 }
26
27 record exec-ctx {
28 tenant: tenant-ctx,
29 flow-id: string,
30 node-id: option<string>,
31 }
32
33 record node-error {
34 code: string,
35 message: string,
36 retryable: bool,
37 backoff-ms: option<u64>,
38 details: option<json>,
39 }
40
41 variant invoke-result {
42 ok(json),
43 err(node-error),
44 }
45
46 variant stream-event {
47 data(json),
48 progress(u8),
49 done,
50 error(string),
51 }
52
53 enum lifecycle-status { ok }
54
55 get-manifest: func() -> json;
56 on-start: func(ctx: exec-ctx) -> result<lifecycle-status, string>;
57 on-stop: func(ctx: exec-ctx, reason: string) -> result<lifecycle-status, string>;
58 invoke: func(ctx: exec-ctx, op: string, input: json) -> invoke-result;
59 invoke-stream: func(ctx: exec-ctx, op: string, input: json) -> list<stream-event>;
60 }
61
62 world component {
63 import control;
64 export node;
65 }
66 "#,
67 world: "component",
68 });
69}
70
71pub mod v0_5 {
72 wasmtime::component::bindgen!({
73 inline: r#"
74 package greentic:component@0.5.0;
75
76 interface control {
77 should-cancel: func() -> bool;
78 yield-now: func();
79 }
80
81 interface node {
82 type json = string;
83
84 record impersonation {
85 actor-id: string,
86 reason: option<string>,
87 }
88
89 record tenant-ctx {
90 env: string,
91 tenant: string,
92 tenant-id: string,
93 team: option<string>,
94 team-id: option<string>,
95 user: option<string>,
96 user-id: option<string>,
97 trace-id: option<string>,
98 i18n-id: option<string>,
99 correlation-id: option<string>,
100 attributes: list<tuple<string, string>>,
101 session-id: option<string>,
102 flow-id: option<string>,
103 node-id: option<string>,
104 provider-id: option<string>,
105 deadline-ms: option<s64>,
106 attempt: u32,
107 idempotency-key: option<string>,
108 impersonation: option<impersonation>,
109 }
110
111 record exec-ctx {
112 tenant: tenant-ctx,
113 i18n-id: option<string>,
114 flow-id: string,
115 node-id: option<string>,
116 }
117
118 record node-error {
119 code: string,
120 message: string,
121 retryable: bool,
122 backoff-ms: option<u64>,
123 details: option<json>,
124 }
125
126 variant invoke-result {
127 ok(json),
128 err(node-error),
129 }
130
131 variant stream-event {
132 data(json),
133 progress(u8),
134 done,
135 error(string),
136 }
137
138 enum lifecycle-status { ok }
139
140 get-manifest: func() -> json;
141 on-start: func(ctx: exec-ctx) -> result<lifecycle-status, string>;
142 on-stop: func(ctx: exec-ctx, reason: string) -> result<lifecycle-status, string>;
143 invoke: func(ctx: exec-ctx, op: string, input: json) -> invoke-result;
144 invoke-stream: func(ctx: exec-ctx, op: string, input: json) -> list<stream-event>;
145 }
146
147 world component {
148 import control;
149 export node;
150 }
151 "#,
152 world: "component",
153 });
154}
155
156pub mod v0_6_descriptor {
157 wasmtime::component::bindgen!({
158 inline: r#"
159 package greentic:component@0.6.0;
160
161 interface component-descriptor {
162 describe: func() -> list<u8>;
163 }
164
165 world component-v0-v6-v0 {
166 export component-descriptor;
167 }
168 "#,
169 world: "component-v0-v6-v0",
170 });
171}
172
173pub mod v0_6 {
176 wasmtime::component::bindgen!({
177 inline: r#"
178 package greentic:component@0.6.0;
179
180 interface node {
181 type capability-id = string;
182 type component-id = string;
183 type flow-id = string;
184 type step-id = string;
185 type tenant-id = string;
186 type team-id = string;
187 type user-id = string;
188 type env-id = string;
189 type trace-id = string;
190 type correlation-id = string;
191
192 record node-error {
193 code: string,
194 message: string,
195 retryable: bool,
196 backoff-ms: option<u64>,
197 details: option<list<u8>>,
198 }
199
200 record tenant-ctx {
201 tenant-id: tenant-id,
202 team-id: option<team-id>,
203 user-id: option<user-id>,
204 env-id: env-id,
205 trace-id: trace-id,
206 correlation-id: correlation-id,
207 deadline-ms: u64,
208 attempt: u32,
209 idempotency-key: option<string>,
210 i18n-id: string,
211 }
212
213 record invocation-envelope {
214 ctx: tenant-ctx,
215 flow-id: flow-id,
216 step-id: step-id,
217 component-id: component-id,
218 attempt: u32,
219 payload-cbor: list<u8>,
220 metadata-cbor: option<list<u8>>,
221 }
222
223 record invocation-result {
224 ok: bool,
225 output-cbor: list<u8>,
226 output-metadata-cbor: option<list<u8>>,
227 }
228
229 invoke: func(op: string, envelope: invocation-envelope) -> result<invocation-result, node-error>;
230 }
231
232 world component {
233 export node;
234 }
235 "#,
236 world: "component",
237 });
238}
239
240pub mod v0_6_runtime {
241 wasmtime::component::bindgen!({
242 inline: r#"
243 package greentic:component@0.6.0;
244
245 interface component-runtime {
246 record run-result {
247 output: list<u8>,
248 new-state: list<u8>,
249 }
250 run: func(input: list<u8>, state: list<u8>) -> run-result;
251 }
252
253 world component-v0-v6-runtime {
254 export component-runtime;
255 }
256 "#,
257 world: "component-v0-v6-runtime",
258 });
259}
260
261pub mod node {
262 pub type Json = String;
263
264 #[derive(Clone, Debug)]
265 pub struct TenantCtx {
266 pub tenant: String,
267 pub team: Option<String>,
268 pub user: Option<String>,
269 pub trace_id: Option<String>,
270 pub i18n_id: Option<String>,
271 pub correlation_id: Option<String>,
272 pub deadline_unix_ms: Option<u64>,
273 pub attempt: u32,
274 pub idempotency_key: Option<String>,
275 }
276
277 #[derive(Clone, Debug)]
278 pub struct ExecCtx {
279 pub tenant: TenantCtx,
280 pub i18n_id: Option<String>,
281 pub flow_id: String,
282 pub node_id: Option<String>,
283 }
284
285 #[derive(Clone, Debug)]
286 pub struct NodeError {
287 pub code: String,
288 pub message: String,
289 pub retryable: bool,
290 pub backoff_ms: Option<u64>,
291 pub details: Option<Json>,
292 }
293
294 #[derive(Clone, Debug)]
295 pub enum InvokeResult {
296 Ok(Json),
297 Err(NodeError),
298 }
299}
300
301pub fn exec_ctx_v0_4(ctx: &node::ExecCtx) -> v0_4::exports::greentic::component::node::ExecCtx {
302 v0_4::exports::greentic::component::node::ExecCtx {
303 tenant: v0_4::exports::greentic::component::node::TenantCtx {
304 tenant: ctx.tenant.tenant.clone(),
305 team: ctx.tenant.team.clone(),
306 user: ctx.tenant.user.clone(),
307 trace_id: ctx.tenant.trace_id.clone(),
308 correlation_id: ctx.tenant.correlation_id.clone(),
309 deadline_unix_ms: ctx.tenant.deadline_unix_ms,
310 attempt: ctx.tenant.attempt,
311 idempotency_key: ctx.tenant.idempotency_key.clone(),
312 },
313 flow_id: ctx.flow_id.clone(),
314 node_id: ctx.node_id.clone(),
315 }
316}
317
318pub fn exec_ctx_v0_5(ctx: &node::ExecCtx) -> v0_5::exports::greentic::component::node::ExecCtx {
319 let env = std::env::var("GREENTIC_ENV").unwrap_or_else(|_| "local".to_string());
320 let team_id = ctx.tenant.team.clone();
321 let user_id = ctx.tenant.user.clone();
322 let deadline_ms = ctx
323 .tenant
324 .deadline_unix_ms
325 .and_then(|value| i64::try_from(value).ok());
326 v0_5::exports::greentic::component::node::ExecCtx {
327 tenant: v0_5::exports::greentic::component::node::TenantCtx {
328 env,
329 tenant: ctx.tenant.tenant.clone(),
330 tenant_id: ctx.tenant.tenant.clone(),
331 team: ctx.tenant.team.clone(),
332 team_id,
333 user: ctx.tenant.user.clone(),
334 user_id,
335 trace_id: ctx.tenant.trace_id.clone(),
336 i18n_id: ctx.tenant.i18n_id.clone(),
337 correlation_id: ctx.tenant.correlation_id.clone(),
338 attributes: Vec::new(),
339 session_id: ctx.tenant.correlation_id.clone(),
340 flow_id: Some(ctx.flow_id.clone()),
341 node_id: ctx.node_id.clone(),
342 provider_id: None,
343 deadline_ms,
344 attempt: ctx.tenant.attempt,
345 idempotency_key: ctx.tenant.idempotency_key.clone(),
346 impersonation: None,
347 },
348 i18n_id: ctx.i18n_id.clone(),
349 flow_id: ctx.flow_id.clone(),
350 node_id: ctx.node_id.clone(),
351 }
352}
353
354pub fn envelope_v0_6(
355 ctx: &node::ExecCtx,
356 component_id: &str,
357 payload_json: &str,
358) -> Result<v0_6::exports::greentic::component::node::InvocationEnvelope> {
359 let env = std::env::var("GREENTIC_ENV").unwrap_or_else(|_| "local".to_string());
360 let step_id = ctx
361 .node_id
362 .clone()
363 .unwrap_or_else(|| "component.exec".to_string());
364 let i18n_id = ctx
365 .i18n_id
366 .clone()
367 .or_else(|| ctx.tenant.i18n_id.clone())
368 .unwrap_or_default();
369 let trace_id = ctx.tenant.trace_id.clone().unwrap_or_default();
370 let correlation_id = ctx.tenant.correlation_id.clone().unwrap_or_default();
371 let deadline_ms = ctx.tenant.deadline_unix_ms.unwrap_or(u64::MAX);
372 let payload_cbor = if let Ok(invocation) =
373 serde_json::from_str::<greentic_types::InvocationEnvelope>(payload_json)
374 {
375 match serde_json::from_slice::<serde_json::Value>(&invocation.payload) {
376 Ok(value) => serde_cbor::to_vec(&value)?,
377 Err(_) => serde_cbor::to_vec(&serde_cbor::Value::Bytes(invocation.payload))?,
378 }
379 } else {
380 let payload_value = serde_json::from_str::<serde_json::Value>(payload_json)
381 .unwrap_or_else(|_| serde_json::Value::String(payload_json.to_string()));
382 serde_cbor::to_vec(&payload_value)?
383 };
384
385 Ok(
386 v0_6::exports::greentic::component::node::InvocationEnvelope {
387 ctx: v0_6::exports::greentic::component::node::TenantCtx {
388 tenant_id: ctx.tenant.tenant.clone(),
389 team_id: ctx.tenant.team.clone(),
390 user_id: ctx.tenant.user.clone(),
391 env_id: env,
392 trace_id,
393 correlation_id,
394 deadline_ms,
395 attempt: ctx.tenant.attempt,
396 idempotency_key: ctx.tenant.idempotency_key.clone(),
397 i18n_id,
398 },
399 flow_id: ctx.flow_id.clone(),
400 step_id,
401 component_id: component_id.to_string(),
402 attempt: ctx.tenant.attempt,
403 payload_cbor,
404 metadata_cbor: None,
405 },
406 )
407}
408
409pub fn invoke_result_from_v0_4(
410 result: v0_4::exports::greentic::component::node::InvokeResult,
411) -> node::InvokeResult {
412 match result {
413 v0_4::exports::greentic::component::node::InvokeResult::Ok(body) => {
414 node::InvokeResult::Ok(body)
415 }
416 v0_4::exports::greentic::component::node::InvokeResult::Err(err) => {
417 node::InvokeResult::Err(node::NodeError {
418 code: err.code,
419 message: err.message,
420 retryable: err.retryable,
421 backoff_ms: err.backoff_ms,
422 details: err.details,
423 })
424 }
425 }
426}
427
428pub fn invoke_result_from_v0_6(
429 result: std::result::Result<
430 v0_6::exports::greentic::component::node::InvocationResult,
431 v0_6::exports::greentic::component::node::NodeError,
432 >,
433) -> Result<node::InvokeResult> {
434 match result {
435 Ok(ok) => {
436 let body = cbor_to_json_string(&ok.output_cbor);
437 if ok.ok {
438 Ok(node::InvokeResult::Ok(body))
439 } else {
440 Ok(node::InvokeResult::Err(node::NodeError {
441 code: "COMPONENT_INVOCATION_FAILED".to_string(),
442 message: body,
443 retryable: false,
444 backoff_ms: None,
445 details: None,
446 }))
447 }
448 }
449 Err(err) => Ok(node::InvokeResult::Err(node::NodeError {
450 code: err.code,
451 message: err.message,
452 retryable: err.retryable,
453 backoff_ms: err.backoff_ms,
454 details: err.details.map(|bytes| cbor_to_json_string(bytes.as_ref())),
455 })),
456 }
457}
458
459pub fn invoke_result_from_v0_5(
460 result: v0_5::exports::greentic::component::node::InvokeResult,
461) -> node::InvokeResult {
462 match result {
463 v0_5::exports::greentic::component::node::InvokeResult::Ok(body) => {
464 node::InvokeResult::Ok(body)
465 }
466 v0_5::exports::greentic::component::node::InvokeResult::Err(err) => {
467 node::InvokeResult::Err(node::NodeError {
468 code: err.code,
469 message: err.message,
470 retryable: err.retryable,
471 backoff_ms: err.backoff_ms,
472 details: err.details,
473 })
474 }
475 }
476}
477
478fn cbor_to_json_string(bytes: &[u8]) -> String {
479 match serde_cbor::from_slice::<serde_json::Value>(bytes)
480 .ok()
481 .and_then(|value| serde_json::to_string(&value).ok())
482 {
483 Some(json) => json,
484 None => String::from_utf8_lossy(bytes).to_string(),
485 }
486}
487
488pub fn invoke_result_from_v0_6_run(
491 result: v0_6_runtime::exports::greentic::component::component_runtime::RunResult,
492) -> node::InvokeResult {
493 let json_str = match serde_cbor::from_slice::<serde_json::Value>(&result.output) {
494 Ok(value) => serde_json::to_string(&value).unwrap_or_default(),
495 Err(_) => {
496 String::from_utf8(result.output).unwrap_or_default()
498 }
499 };
500 node::InvokeResult::Ok(json_str)
501}
502
503#[cfg(test)]
504mod tests {
505 use super::{envelope_v0_6, node};
506 use greentic_types::{EnvId, InvocationEnvelope, TenantCtx, TenantId};
507 use std::str::FromStr;
508
509 fn sample_exec_ctx() -> node::ExecCtx {
510 node::ExecCtx {
511 tenant: node::TenantCtx {
512 tenant: "tenant.demo".to_string(),
513 team: Some("team.demo".to_string()),
514 user: Some("user.demo".to_string()),
515 trace_id: Some("trace.demo".to_string()),
516 i18n_id: Some("en-US".to_string()),
517 correlation_id: Some("corr.demo".to_string()),
518 deadline_unix_ms: Some(123),
519 attempt: 2,
520 idempotency_key: Some("idem.demo".to_string()),
521 },
522 i18n_id: Some("en-US".to_string()),
523 flow_id: "flow.demo".to_string(),
524 node_id: Some("node.demo".to_string()),
525 }
526 }
527
528 #[test]
529 fn envelope_v0_6_preserves_binary_payload_when_envelope_payload_is_not_json() {
530 let envelope = InvocationEnvelope {
531 ctx: TenantCtx::new(
532 EnvId::from_str("local").expect("valid env id"),
533 TenantId::from_str("tenant.default").expect("valid tenant id"),
534 ),
535 flow_id: "flow.demo".to_string(),
536 node_id: Some("node.demo".to_string()),
537 op: "on_message".to_string(),
538 payload: vec![255, 0, 1, 42],
539 metadata: Vec::new(),
540 };
541 let payload_json = serde_json::to_string(&envelope).expect("serialize invocation envelope");
542 let envelope = envelope_v0_6(&sample_exec_ctx(), "component.demo", &payload_json)
543 .expect("envelope conversion");
544 let decoded: serde_cbor::Value =
545 serde_cbor::from_slice(&envelope.payload_cbor).expect("decode cbor");
546 assert_eq!(
547 decoded,
548 serde_cbor::Value::Bytes(vec![255, 0, 1, 42]),
549 "binary payload must not be dropped when not valid json bytes"
550 );
551 }
552}