Skip to main content

greentic_interfaces/
mappers.rs

1//! Conversion helpers between generated WIT bindings and `greentic-types`.
2
3use std::collections::BTreeMap;
4use std::convert::{TryFrom, TryInto};
5
6use greentic_types as types;
7use semver::Version;
8use time::OffsetDateTime;
9
10use crate::bindings;
11
12type MapperResult<T> = Result<T, types::GreenticError>;
13
14fn invalid_input(msg: impl Into<String>) -> types::GreenticError {
15    types::GreenticError::new(types::ErrorCode::InvalidInput, msg)
16}
17
18fn i128_to_i64(value: i128) -> MapperResult<i64> {
19    value
20        .try_into()
21        .map_err(|_| invalid_input("numeric overflow converting deadline"))
22}
23
24fn timestamp_ms_to_offset(ms: i64) -> MapperResult<OffsetDateTime> {
25    let nanos = (ms as i128)
26        .checked_mul(1_000_000)
27        .ok_or_else(|| invalid_input("timestamp overflow"))?;
28    OffsetDateTime::from_unix_timestamp_nanos(nanos)
29        .map_err(|err| invalid_input(format!("invalid timestamp: {err}")))
30}
31
32fn offset_to_timestamp_ms(dt: &OffsetDateTime) -> MapperResult<i64> {
33    let nanos = dt.unix_timestamp_nanos();
34    let ms = nanos
35        .checked_div(1_000_000)
36        .ok_or_else(|| invalid_input("timestamp division overflow"))?;
37    ms.try_into()
38        .map_err(|_| invalid_input("timestamp overflow converting to milliseconds"))
39}
40
41type WitTenantCtx = crate::canonical::types::TenantCtx;
42type WitImpersonation = crate::canonical::types::Impersonation;
43type WitSessionCursor = crate::canonical::types::SessionCursor;
44type WitOutcome = crate::canonical::types::Outcome;
45type WitOutcomePending = crate::canonical::types::OutcomePending;
46type WitOutcomeError = crate::canonical::types::OutcomeError;
47type WitErrorCode = crate::canonical::types::ErrorCode;
48type WitAllowList = crate::canonical::types::AllowList;
49type WitProtocol = crate::canonical::types::Protocol;
50type WitNetworkPolicy = crate::canonical::types::NetworkPolicy;
51type WitSpanContext = crate::canonical::types::SpanContext;
52type WitPackRef = crate::canonical::types::PackRef;
53type WitSignature = crate::canonical::types::Signature;
54type WitSignatureAlgorithm = crate::canonical::types::SignatureAlgorithm;
55type WitCommonFlowKind =
56    bindings::greentic_common_types_0_1_0_common::exports::greentic::common_types::types::FlowKind;
57type WitCommonTenantCtx =
58    bindings::greentic_common_types_0_1_0_common::exports::greentic::common_types::types::TenantCtx;
59type WitOutcomeStatus = bindings::greentic_common_types_0_1_0_common::exports::greentic::common_types::types::OutcomeStatus;
60type WitComponentOutcome = bindings::greentic_common_types_0_1_0_common::exports::greentic::common_types::types::ComponentOutcome;
61type WitPackKind = bindings::greentic_pack_export_v1_0_1_0_pack_host::exports::greentic::pack_export_v1::pack_api::PackKind;
62type WitPackDescriptor =
63    bindings::greentic_pack_export_v1_0_1_0_pack_host::exports::greentic::pack_export_v1::pack_api::PackDescriptor;
64type WitFlowDescriptor =
65    bindings::greentic_pack_export_v1_0_1_0_pack_host::exports::greentic::pack_export_v1::pack_api::FlowDescriptor;
66type WitPackFlowKind =
67    bindings::greentic_pack_export_v1_0_1_0_pack_host::greentic::common_types::types::FlowKind;
68
69/// Normalized component outcome mirroring `greentic:common-types/component-outcome`.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum ComponentOutcomeStatus {
72    /// Component finished successfully.
73    Done,
74    /// Component needs more input.
75    Pending,
76    /// Component failed.
77    Error,
78}
79
80/// Component outcome payload used by the v1 ABI.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ComponentOutcome {
83    /// Status reported by the component.
84    pub status: ComponentOutcomeStatus,
85    /// Optional routing code.
86    pub code: Option<String>,
87    /// JSON payload returned by the component.
88    pub payload: String,
89    /// Optional metadata JSON blob.
90    pub metadata: Option<String>,
91}
92
93/// Minimal pack descriptor mirroring the v1 pack-export ABI.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct PackDescriptor {
96    /// Logical pack identifier.
97    pub pack_id: types::PackId,
98    /// Pack version.
99    pub version: Version,
100    /// Pack kind classification.
101    pub kind: types::PackKind,
102    /// Declared publisher.
103    pub publisher: String,
104}
105
106/// Minimal flow descriptor mirroring the v1 pack-export ABI.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct FlowDescriptor {
109    /// Flow identifier.
110    pub id: types::FlowId,
111    /// Flow kind classification.
112    pub kind: types::FlowKind,
113    /// Flow tags.
114    pub tags: Vec<String>,
115    /// Flow entrypoints.
116    pub entrypoints: Vec<String>,
117}
118
119/// Convert a WIT `TenantCtx` into the shared `greentic_types::TenantCtx`.
120pub fn tenant_ctx_from_wit(ctx: WitTenantCtx) -> MapperResult<types::TenantCtx> {
121    types::TenantCtx::try_from(ctx)
122}
123
124/// Convert a shared `greentic_types::TenantCtx` into the WIT `TenantCtx`.
125pub fn tenant_ctx_to_wit(ctx: types::TenantCtx) -> MapperResult<WitTenantCtx> {
126    WitTenantCtx::try_from(ctx)
127}
128
129impl TryFrom<WitImpersonation> for types::Impersonation {
130    type Error = types::GreenticError;
131
132    fn try_from(value: WitImpersonation) -> MapperResult<Self> {
133        Ok(Self {
134            actor_id: value.actor_id.try_into()?,
135            reason: value.reason,
136        })
137    }
138}
139
140impl From<types::Impersonation> for WitImpersonation {
141    fn from(value: types::Impersonation) -> Self {
142        Self {
143            actor_id: value.actor_id.into(),
144            reason: value.reason,
145        }
146    }
147}
148
149impl TryFrom<WitTenantCtx> for types::TenantCtx {
150    type Error = types::GreenticError;
151
152    fn try_from(value: WitTenantCtx) -> MapperResult<Self> {
153        let WitTenantCtx {
154            env,
155            tenant,
156            tenant_id,
157            team,
158            team_id,
159            user,
160            user_id,
161            trace_id,
162            i18n_id,
163            correlation_id,
164            session_id,
165            flow_id,
166            node_id,
167            provider_id,
168            deadline_ms,
169            attempt,
170            idempotency_key,
171            impersonation,
172            attributes,
173        } = value;
174
175        let deadline =
176            deadline_ms.map(|ms| types::InvocationDeadline::from_unix_millis(ms as i128));
177
178        let env = env.try_into()?;
179        let tenant = tenant.try_into()?;
180        let tenant_id = tenant_id.try_into()?;
181        let team = team.map(|item| item.try_into()).transpose()?;
182        let team_id = team_id.map(|item| item.try_into()).transpose()?;
183        let user = user.map(|item| item.try_into()).transpose()?;
184        let user_id = user_id.map(|item| item.try_into()).transpose()?;
185        let impersonation = impersonation
186            .map(types::Impersonation::try_from)
187            .transpose()?;
188        let attributes: BTreeMap<String, String> = attributes.into_iter().collect();
189
190        Ok(Self {
191            env,
192            tenant,
193            tenant_id,
194            team,
195            team_id,
196            user,
197            user_id,
198            session_id,
199            flow_id,
200            node_id,
201            provider_id,
202            trace_id,
203            i18n_id,
204            correlation_id,
205            attributes,
206            deadline,
207            attempt,
208            idempotency_key,
209            impersonation,
210        })
211    }
212}
213
214impl TryFrom<types::TenantCtx> for WitTenantCtx {
215    type Error = types::GreenticError;
216
217    fn try_from(value: types::TenantCtx) -> MapperResult<Self> {
218        let deadline_ms = match value.deadline {
219            Some(deadline) => Some(i128_to_i64(deadline.unix_millis())?),
220            None => None,
221        };
222        let attributes: Vec<(String, String)> = value.attributes.into_iter().collect();
223
224        Ok(Self {
225            env: value.env.into(),
226            tenant: value.tenant.into(),
227            tenant_id: value.tenant_id.into(),
228            team: value.team.map(Into::into),
229            team_id: value.team_id.map(Into::into),
230            user: value.user.map(Into::into),
231            user_id: value.user_id.map(Into::into),
232            session_id: value.session_id,
233            flow_id: value.flow_id,
234            node_id: value.node_id,
235            provider_id: value.provider_id,
236            trace_id: value.trace_id,
237            i18n_id: value.i18n_id.clone(),
238            correlation_id: value.correlation_id,
239            attributes,
240            deadline_ms,
241            attempt: value.attempt,
242            idempotency_key: value.idempotency_key,
243            impersonation: value.impersonation.map(Into::into),
244        })
245    }
246}
247
248impl From<WitSessionCursor> for types::SessionCursor {
249    fn from(value: WitSessionCursor) -> Self {
250        Self {
251            node_pointer: value.node_pointer,
252            wait_reason: value.wait_reason,
253            outbox_marker: value.outbox_marker,
254        }
255    }
256}
257
258impl From<types::SessionCursor> for WitSessionCursor {
259    fn from(value: types::SessionCursor) -> Self {
260        Self {
261            node_pointer: value.node_pointer,
262            wait_reason: value.wait_reason,
263            outbox_marker: value.outbox_marker,
264        }
265    }
266}
267
268impl From<WitErrorCode> for types::ErrorCode {
269    fn from(value: WitErrorCode) -> Self {
270        match value {
271            WitErrorCode::Unknown => Self::Unknown,
272            WitErrorCode::InvalidInput => Self::InvalidInput,
273            WitErrorCode::NotFound => Self::NotFound,
274            WitErrorCode::Conflict => Self::Conflict,
275            WitErrorCode::Timeout => Self::Timeout,
276            WitErrorCode::Unauthenticated => Self::Unauthenticated,
277            WitErrorCode::PermissionDenied => Self::PermissionDenied,
278            WitErrorCode::RateLimited => Self::RateLimited,
279            WitErrorCode::Unavailable => Self::Unavailable,
280            WitErrorCode::Internal => Self::Internal,
281        }
282    }
283}
284
285impl From<types::ErrorCode> for WitErrorCode {
286    fn from(value: types::ErrorCode) -> Self {
287        match value {
288            types::ErrorCode::Unknown => Self::Unknown,
289            types::ErrorCode::InvalidInput => Self::InvalidInput,
290            types::ErrorCode::NotFound => Self::NotFound,
291            types::ErrorCode::Conflict => Self::Conflict,
292            types::ErrorCode::Timeout => Self::Timeout,
293            types::ErrorCode::Unauthenticated => Self::Unauthenticated,
294            types::ErrorCode::PermissionDenied => Self::PermissionDenied,
295            types::ErrorCode::RateLimited => Self::RateLimited,
296            types::ErrorCode::Unavailable => Self::Unavailable,
297            types::ErrorCode::Internal => Self::Internal,
298        }
299    }
300}
301
302impl From<WitOutcome> for types::Outcome<String> {
303    fn from(value: WitOutcome) -> Self {
304        match value {
305            WitOutcome::Done(val) => Self::Done(val),
306            WitOutcome::Pending(payload) => Self::Pending {
307                reason: payload.reason,
308                expected_input: payload.expected_input,
309            },
310            WitOutcome::Error(payload) => Self::Error {
311                code: payload.code.into(),
312                message: payload.message,
313            },
314        }
315    }
316}
317
318impl From<types::Outcome<String>> for WitOutcome {
319    fn from(value: types::Outcome<String>) -> Self {
320        match value {
321            types::Outcome::Done(val) => Self::Done(val),
322            types::Outcome::Pending {
323                reason,
324                expected_input,
325            } => Self::Pending(WitOutcomePending {
326                reason,
327                expected_input,
328            }),
329            types::Outcome::Error { code, message } => Self::Error(WitOutcomeError {
330                code: code.into(),
331                message,
332            }),
333        }
334    }
335}
336
337impl From<WitProtocol> for types::Protocol {
338    fn from(value: WitProtocol) -> Self {
339        match value {
340            WitProtocol::Http => Self::Http,
341            WitProtocol::Https => Self::Https,
342            WitProtocol::Tcp => Self::Tcp,
343            WitProtocol::Udp => Self::Udp,
344            WitProtocol::Grpc => Self::Grpc,
345            WitProtocol::Custom(v) => Self::Custom(v),
346        }
347    }
348}
349
350impl From<types::Protocol> for WitProtocol {
351    fn from(value: types::Protocol) -> Self {
352        match value {
353            types::Protocol::Http => Self::Http,
354            types::Protocol::Https => Self::Https,
355            types::Protocol::Tcp => Self::Tcp,
356            types::Protocol::Udp => Self::Udp,
357            types::Protocol::Grpc => Self::Grpc,
358            types::Protocol::Custom(v) => Self::Custom(v),
359        }
360    }
361}
362
363impl From<WitAllowList> for types::AllowList {
364    fn from(value: WitAllowList) -> Self {
365        Self {
366            domains: value.domains,
367            ports: value.ports,
368            protocols: value.protocols.into_iter().map(Into::into).collect(),
369        }
370    }
371}
372
373impl From<types::AllowList> for WitAllowList {
374    fn from(value: types::AllowList) -> Self {
375        Self {
376            domains: value.domains,
377            ports: value.ports,
378            protocols: value.protocols.into_iter().map(Into::into).collect(),
379        }
380    }
381}
382
383impl From<WitNetworkPolicy> for types::NetworkPolicy {
384    fn from(value: WitNetworkPolicy) -> Self {
385        Self {
386            egress: value.egress.into(),
387            deny_on_miss: value.deny_on_miss,
388        }
389    }
390}
391
392impl From<types::NetworkPolicy> for WitNetworkPolicy {
393    fn from(value: types::NetworkPolicy) -> Self {
394        Self {
395            egress: value.egress.into(),
396            deny_on_miss: value.deny_on_miss,
397        }
398    }
399}
400
401impl TryFrom<WitSpanContext> for types::SpanContext {
402    type Error = types::GreenticError;
403
404    fn try_from(value: WitSpanContext) -> MapperResult<Self> {
405        let WitSpanContext {
406            tenant,
407            session_id,
408            flow_id,
409            node_id,
410            provider,
411            start_ms,
412            end_ms,
413        } = value;
414
415        let start = start_ms.map(timestamp_ms_to_offset).transpose()?;
416        let end = end_ms.map(timestamp_ms_to_offset).transpose()?;
417        let tenant = tenant.try_into()?;
418
419        Ok(Self {
420            tenant,
421            session_id: session_id.map(types::SessionKey::from),
422            flow_id,
423            node_id,
424            provider,
425            start,
426            end,
427        })
428    }
429}
430
431impl TryFrom<types::SpanContext> for WitSpanContext {
432    type Error = types::GreenticError;
433
434    fn try_from(value: types::SpanContext) -> MapperResult<Self> {
435        let start_ms = value
436            .start
437            .as_ref()
438            .map(offset_to_timestamp_ms)
439            .transpose()?;
440        let end_ms = value.end.as_ref().map(offset_to_timestamp_ms).transpose()?;
441
442        Ok(Self {
443            tenant: value.tenant.into(),
444            session_id: value.session_id.map(|key| key.to_string()),
445            flow_id: value.flow_id,
446            node_id: value.node_id,
447            provider: value.provider,
448            start_ms,
449            end_ms,
450        })
451    }
452}
453
454impl From<WitSignatureAlgorithm> for types::SignatureAlgorithm {
455    fn from(value: WitSignatureAlgorithm) -> Self {
456        match value {
457            WitSignatureAlgorithm::Ed25519 => Self::Ed25519,
458            WitSignatureAlgorithm::Other(v) => Self::Other(v),
459        }
460    }
461}
462
463impl From<types::SignatureAlgorithm> for WitSignatureAlgorithm {
464    fn from(value: types::SignatureAlgorithm) -> Self {
465        match value {
466            types::SignatureAlgorithm::Ed25519 => Self::Ed25519,
467            types::SignatureAlgorithm::Other(v) => Self::Other(v),
468        }
469    }
470}
471
472impl From<WitSignature> for types::Signature {
473    fn from(value: WitSignature) -> Self {
474        Self {
475            key_id: value.key_id,
476            algorithm: value.algorithm.into(),
477            signature: value.signature,
478        }
479    }
480}
481
482impl From<types::Signature> for WitSignature {
483    fn from(value: types::Signature) -> Self {
484        Self {
485            key_id: value.key_id,
486            algorithm: value.algorithm.into(),
487            signature: value.signature,
488        }
489    }
490}
491
492impl TryFrom<WitPackRef> for types::PackRef {
493    type Error = types::GreenticError;
494
495    fn try_from(value: WitPackRef) -> MapperResult<Self> {
496        let version = Version::parse(&value.version)
497            .map_err(|err| invalid_input(format!("invalid version: {err}")))?;
498        Ok(Self {
499            oci_url: value.oci_url,
500            version,
501            digest: value.digest,
502            signatures: value.signatures.into_iter().map(Into::into).collect(),
503        })
504    }
505}
506
507impl From<types::PackRef> for WitPackRef {
508    fn from(value: types::PackRef) -> Self {
509        Self {
510            oci_url: value.oci_url,
511            version: value.version.to_string(),
512            digest: value.digest,
513            signatures: value.signatures.into_iter().map(Into::into).collect(),
514        }
515    }
516}
517
518/// Convert the shared `FlowKind` into the WIT `flow-kind`.
519pub fn flow_kind_to_wit(kind: types::FlowKind) -> WitCommonFlowKind {
520    match kind {
521        types::FlowKind::Messaging => WitCommonFlowKind::Messaging,
522        types::FlowKind::Event => WitCommonFlowKind::Event,
523        types::FlowKind::ComponentConfig => WitCommonFlowKind::ComponentConfig,
524        types::FlowKind::Job => WitCommonFlowKind::Job,
525        types::FlowKind::Http => WitCommonFlowKind::Http,
526    }
527}
528
529/// Convert a WIT `flow-kind` into the shared `FlowKind`.
530pub fn flow_kind_from_wit(kind: WitCommonFlowKind) -> types::FlowKind {
531    match kind {
532        WitCommonFlowKind::Messaging => types::FlowKind::Messaging,
533        WitCommonFlowKind::Event => types::FlowKind::Event,
534        WitCommonFlowKind::ComponentConfig => types::FlowKind::ComponentConfig,
535        WitCommonFlowKind::Job => types::FlowKind::Job,
536        WitCommonFlowKind::Http => types::FlowKind::Http,
537    }
538}
539
540fn flow_kind_from_pack_wit(kind: WitPackFlowKind) -> types::FlowKind {
541    match kind {
542        WitPackFlowKind::Messaging => types::FlowKind::Messaging,
543        WitPackFlowKind::Event => types::FlowKind::Event,
544        WitPackFlowKind::ComponentConfig => types::FlowKind::ComponentConfig,
545        WitPackFlowKind::Job => types::FlowKind::Job,
546        WitPackFlowKind::Http => types::FlowKind::Http,
547    }
548}
549
550fn flow_kind_to_pack_wit(kind: types::FlowKind) -> WitPackFlowKind {
551    match kind {
552        types::FlowKind::Messaging => WitPackFlowKind::Messaging,
553        types::FlowKind::Event => WitPackFlowKind::Event,
554        types::FlowKind::ComponentConfig => WitPackFlowKind::ComponentConfig,
555        types::FlowKind::Job => WitPackFlowKind::Job,
556        types::FlowKind::Http => WitPackFlowKind::Http,
557    }
558}
559
560/// Convert the shared `PackKind` into the WIT `pack-kind`.
561pub fn pack_kind_to_wit(kind: types::PackKind) -> WitPackKind {
562    match kind {
563        types::PackKind::Application => WitPackKind::Application,
564        types::PackKind::Provider => WitPackKind::Provider,
565        types::PackKind::Infrastructure => WitPackKind::Infrastructure,
566        types::PackKind::Library => WitPackKind::Library,
567    }
568}
569
570/// Convert a WIT `pack-kind` into the shared `PackKind`.
571pub fn pack_kind_from_wit(kind: WitPackKind) -> types::PackKind {
572    match kind {
573        WitPackKind::Application => types::PackKind::Application,
574        WitPackKind::Provider => types::PackKind::Provider,
575        WitPackKind::Infrastructure => types::PackKind::Infrastructure,
576        WitPackKind::Library => types::PackKind::Library,
577    }
578}
579
580/// Convert a WIT `tenant-ctx` (v1 subset) into the shared `TenantCtx`.
581pub fn tenant_ctx_from_common(ctx: WitCommonTenantCtx) -> MapperResult<types::TenantCtx> {
582    let WitCommonTenantCtx {
583        env,
584        tenant_id,
585        team_id,
586        user_id,
587        i18n_id,
588        session_id,
589        flow_id,
590        node_id,
591    } = ctx;
592    let tenant_id: types::TenantId = tenant_id.try_into()?;
593    let tenant = tenant_id.clone();
594    let team = team_id
595        .as_ref()
596        .map(|id| id.as_str().try_into())
597        .transpose()?;
598    let team_id = team_id.map(|id| id.try_into()).transpose()?;
599    let user = user_id
600        .as_ref()
601        .map(|id| id.as_str().try_into())
602        .transpose()?;
603    let user_id = user_id.map(|id| id.try_into()).transpose()?;
604
605    Ok(types::TenantCtx {
606        env: env.try_into()?,
607        tenant,
608        tenant_id,
609        team,
610        team_id,
611        user,
612        user_id,
613        session_id,
614        flow_id,
615        node_id,
616        provider_id: None,
617        trace_id: None,
618        i18n_id,
619        correlation_id: None,
620        attributes: BTreeMap::new(),
621        deadline: None,
622        attempt: 0,
623        idempotency_key: None,
624        impersonation: None,
625    })
626}
627
628/// Convert a shared `TenantCtx` into the WIT `tenant-ctx` (v1 subset).
629pub fn tenant_ctx_to_common(ctx: types::TenantCtx) -> MapperResult<WitCommonTenantCtx> {
630    Ok(WitCommonTenantCtx {
631        env: ctx.env.into(),
632        tenant_id: ctx.tenant_id.into(),
633        team_id: ctx.team_id.map(Into::into),
634        user_id: ctx.user_id.map(Into::into),
635        i18n_id: ctx.i18n_id,
636        session_id: ctx.session_id,
637        flow_id: ctx.flow_id,
638        node_id: ctx.node_id,
639    })
640}
641
642/// Convert a WIT `component-outcome` into the normalized struct.
643pub fn component_outcome_from_wit(outcome: WitComponentOutcome) -> ComponentOutcome {
644    let status = match outcome.status {
645        WitOutcomeStatus::Done => ComponentOutcomeStatus::Done,
646        WitOutcomeStatus::Pending => ComponentOutcomeStatus::Pending,
647        WitOutcomeStatus::Error => ComponentOutcomeStatus::Error,
648    };
649
650    ComponentOutcome {
651        status,
652        code: outcome.code,
653        payload: outcome.payload_json,
654        metadata: outcome.metadata_json,
655    }
656}
657
658/// Convert a normalized component outcome into the WIT `component-outcome`.
659pub fn component_outcome_to_wit(outcome: ComponentOutcome) -> WitComponentOutcome {
660    let status = match outcome.status {
661        ComponentOutcomeStatus::Done => WitOutcomeStatus::Done,
662        ComponentOutcomeStatus::Pending => WitOutcomeStatus::Pending,
663        ComponentOutcomeStatus::Error => WitOutcomeStatus::Error,
664    };
665
666    WitComponentOutcome {
667        status,
668        code: outcome.code,
669        payload_json: outcome.payload,
670        metadata_json: outcome.metadata,
671    }
672}
673
674/// Convert a WIT pack descriptor into the shared struct.
675pub fn pack_descriptor_from_wit(desc: WitPackDescriptor) -> MapperResult<PackDescriptor> {
676    Ok(PackDescriptor {
677        pack_id: desc.pack_id.try_into()?,
678        version: Version::parse(&desc.version)
679            .map_err(|err| invalid_input(format!("invalid version: {err}")))?,
680        kind: pack_kind_from_wit(desc.kind),
681        publisher: desc.publisher,
682    })
683}
684
685/// Convert a pack descriptor into the WIT shape.
686pub fn pack_descriptor_to_wit(desc: PackDescriptor) -> WitPackDescriptor {
687    WitPackDescriptor {
688        pack_id: desc.pack_id.into(),
689        version: desc.version.to_string(),
690        kind: pack_kind_to_wit(desc.kind),
691        publisher: desc.publisher,
692    }
693}
694
695/// Convert a WIT flow descriptor into the shared struct.
696pub fn flow_descriptor_from_wit(desc: WitFlowDescriptor) -> MapperResult<FlowDescriptor> {
697    Ok(FlowDescriptor {
698        id: desc.id.try_into()?,
699        kind: flow_kind_from_pack_wit(desc.kind),
700        tags: desc.tags,
701        entrypoints: desc.entrypoints,
702    })
703}
704
705/// Convert a flow descriptor into the WIT shape.
706pub fn flow_descriptor_to_wit(desc: FlowDescriptor) -> WitFlowDescriptor {
707    WitFlowDescriptor {
708        id: desc.id.into(),
709        kind: flow_kind_to_pack_wit(desc.kind),
710        tags: desc.tags,
711        entrypoints: desc.entrypoints,
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718    use std::convert::TryFrom;
719
720    fn fixture_id<T>(value: &str) -> T
721    where
722        T: TryFrom<String, Error = types::GreenticError>,
723    {
724        T::try_from(value.to_owned())
725            .unwrap_or_else(|err| panic!("invalid fixture identifier '{value}': {err}"))
726    }
727
728    fn sample_tenant_ctx() -> types::TenantCtx {
729        types::TenantCtx {
730            env: fixture_id("prod"),
731            tenant: fixture_id("tenant-1"),
732            tenant_id: fixture_id("tenant-1"),
733            team: Some(fixture_id("team-42")),
734            team_id: Some(fixture_id("team-42")),
735            user: Some(fixture_id("user-7")),
736            user_id: Some(fixture_id("user-7")),
737            attributes: BTreeMap::new(),
738            session_id: Some("sess-42".into()),
739            flow_id: Some("flow-42".into()),
740            node_id: Some("node-42".into()),
741            provider_id: Some("provider-42".into()),
742            trace_id: Some("trace".into()),
743            i18n_id: Some("en-US".into()),
744            correlation_id: Some("corr".into()),
745            deadline: Some(types::InvocationDeadline::from_unix_millis(
746                1_700_000_000_000,
747            )),
748            attempt: 2,
749            idempotency_key: Some("idem".into()),
750            impersonation: Some(types::Impersonation {
751                actor_id: fixture_id("actor"),
752                reason: Some("maintenance".into()),
753            }),
754        }
755    }
756
757    #[test]
758    fn tenant_ctx_roundtrip() {
759        let ctx = sample_tenant_ctx();
760        let wit = match WitTenantCtx::try_from(ctx.clone()) {
761            Ok(value) => value,
762            Err(err) => panic!("failed to map to wit: {err}"),
763        };
764        let back = match types::TenantCtx::try_from(wit) {
765            Ok(value) => value,
766            Err(err) => panic!("failed to map from wit: {err}"),
767        };
768        assert_eq!(back.env.as_str(), ctx.env.as_str());
769        assert_eq!(back.tenant.as_str(), ctx.tenant.as_str());
770        assert!(back.impersonation.is_some());
771        assert!(ctx.impersonation.is_some());
772        assert_eq!(
773            back.impersonation.as_ref().map(|imp| imp.actor_id.as_str()),
774            ctx.impersonation.as_ref().map(|imp| imp.actor_id.as_str())
775        );
776        assert_eq!(back.session_id, ctx.session_id);
777        assert_eq!(back.flow_id, ctx.flow_id);
778        assert_eq!(back.node_id, ctx.node_id);
779        assert_eq!(back.provider_id, ctx.provider_id);
780    }
781
782    #[test]
783    fn outcome_roundtrip() {
784        let pending = types::Outcome::Pending {
785            reason: "waiting".into(),
786            expected_input: Some(vec!["input".into()]),
787        };
788        let wit = WitOutcome::from(pending.clone());
789        let back = types::Outcome::from(wit);
790        match back {
791            types::Outcome::Pending { reason, .. } => {
792                assert_eq!(reason, "waiting");
793            }
794            _ => panic!("expected pending"),
795        }
796    }
797
798    #[test]
799    fn outcome_error_codes_roundtrip_all_variants() {
800        let codes = [
801            types::ErrorCode::Unknown,
802            types::ErrorCode::InvalidInput,
803            types::ErrorCode::NotFound,
804            types::ErrorCode::Conflict,
805            types::ErrorCode::Timeout,
806            types::ErrorCode::Unauthenticated,
807            types::ErrorCode::PermissionDenied,
808            types::ErrorCode::RateLimited,
809            types::ErrorCode::Unavailable,
810            types::ErrorCode::Internal,
811        ];
812
813        for code in codes {
814            let wit_code = WitErrorCode::from(code);
815            let round = types::ErrorCode::from(wit_code);
816            assert_eq!(round, code);
817        }
818
819        let done: WitOutcome = types::Outcome::Done("ok".to_string()).into();
820        assert!(
821            matches!(types::Outcome::<String>::from(done), types::Outcome::Done(value) if value == "ok")
822        );
823
824        let error: WitOutcome = types::Outcome::Error {
825            code: types::ErrorCode::PermissionDenied,
826            message: "denied".into(),
827        }
828        .into();
829        assert!(matches!(
830            types::Outcome::<String>::from(error),
831            types::Outcome::Error {
832                code: types::ErrorCode::PermissionDenied,
833                message
834            } if message == "denied"
835        ));
836    }
837
838    #[test]
839    fn connectivity_policy_roundtrips_all_protocol_variants() {
840        let protocols = vec![
841            types::Protocol::Http,
842            types::Protocol::Https,
843            types::Protocol::Tcp,
844            types::Protocol::Udp,
845            types::Protocol::Grpc,
846            types::Protocol::Custom("mqtt".into()),
847        ];
848        let policy = types::NetworkPolicy {
849            egress: types::AllowList {
850                domains: vec!["example.com".into()],
851                ports: vec![443, 8443],
852                protocols: protocols.clone(),
853            },
854            deny_on_miss: true,
855        };
856
857        let wit = WitNetworkPolicy::from(policy.clone());
858        let round = types::NetworkPolicy::from(wit);
859        assert_eq!(round.deny_on_miss, policy.deny_on_miss);
860        assert_eq!(round.egress.domains, policy.egress.domains);
861        assert_eq!(round.egress.ports, policy.egress.ports);
862        assert_eq!(round.egress.protocols, protocols);
863    }
864
865    #[test]
866    fn span_context_rejects_unrepresentable_timestamps() {
867        let wit = WitSpanContext {
868            tenant: "tenant".into(),
869            session_id: None,
870            flow_id: "flow".into(),
871            node_id: None,
872            provider: "provider".into(),
873            start_ms: Some(i64::MAX),
874            end_ms: None,
875        };
876
877        let err = match types::SpanContext::try_from(wit) {
878            Ok(_) => panic!("timestamp should be invalid"),
879            Err(err) => err,
880        };
881        assert!(
882            err.to_string().contains("timestamp"),
883            "unexpected error: {err}"
884        );
885    }
886
887    #[test]
888    fn pack_ref_rejects_invalid_version() {
889        let wit = WitPackRef {
890            oci_url: "registry.example.com/pack".into(),
891            version: "not-semver".into(),
892            digest: "sha256:deadbeef".into(),
893            signatures: Vec::new(),
894        };
895
896        let err = match types::PackRef::try_from(wit) {
897            Ok(_) => panic!("invalid version should fail"),
898            Err(err) => err,
899        };
900        assert!(
901            err.to_string().contains("invalid version"),
902            "unexpected error: {err}"
903        );
904    }
905
906    #[test]
907    fn common_tenant_ctx_preserves_subset_and_defaults_missing_fields() {
908        let wit = WitCommonTenantCtx {
909            env: "prod".into(),
910            tenant_id: "tenant-1".into(),
911            team_id: Some("team-1".into()),
912            user_id: Some("user-1".into()),
913            i18n_id: Some("nl-NL".into()),
914            session_id: Some("session".into()),
915            flow_id: Some("flow".into()),
916            node_id: Some("node".into()),
917        };
918
919        let ctx = match tenant_ctx_from_common(wit) {
920            Ok(ctx) => ctx,
921            Err(err) => panic!("common tenant ctx rejected: {err}"),
922        };
923        assert_eq!(ctx.env.as_str(), "prod");
924        assert_eq!(ctx.tenant.as_str(), "tenant-1");
925        assert_eq!(ctx.team.as_ref().map(|id| id.as_str()), Some("team-1"));
926        assert_eq!(ctx.user.as_ref().map(|id| id.as_str()), Some("user-1"));
927        assert_eq!(ctx.attempt, 0);
928        assert!(ctx.attributes.is_empty());
929        assert!(ctx.impersonation.is_none());
930
931        let round = match tenant_ctx_to_common(ctx) {
932            Ok(ctx) => ctx,
933            Err(err) => panic!("common tenant ctx conversion failed: {err}"),
934        };
935        assert_eq!(round.tenant_id, "tenant-1");
936        assert_eq!(round.team_id.as_deref(), Some("team-1"));
937        assert_eq!(round.user_id.as_deref(), Some("user-1"));
938        assert_eq!(round.session_id.as_deref(), Some("session"));
939    }
940
941    #[test]
942    fn component_outcome_maps_status_and_payload_fields() {
943        for (status, wit_status) in [
944            (ComponentOutcomeStatus::Done, WitOutcomeStatus::Done),
945            (ComponentOutcomeStatus::Pending, WitOutcomeStatus::Pending),
946            (ComponentOutcomeStatus::Error, WitOutcomeStatus::Error),
947        ] {
948            let outcome = ComponentOutcome {
949                status: status.clone(),
950                code: Some("code".into()),
951                payload: "{\"ok\":true}".into(),
952                metadata: Some("{\"trace\":\"1\"}".into()),
953            };
954            let wit = component_outcome_to_wit(outcome.clone());
955            assert_eq!(wit.status, wit_status);
956            let round = component_outcome_from_wit(wit);
957            assert_eq!(round, outcome);
958        }
959    }
960
961    #[test]
962    fn pack_and_flow_descriptors_roundtrip_all_kind_variants() {
963        for kind in [
964            types::PackKind::Application,
965            types::PackKind::Provider,
966            types::PackKind::Infrastructure,
967            types::PackKind::Library,
968        ] {
969            let desc = PackDescriptor {
970                pack_id: fixture_id("pack"),
971                version: match Version::parse("1.2.3") {
972                    Ok(version) => version,
973                    Err(err) => panic!("valid version rejected: {err}"),
974                },
975                kind,
976                publisher: "greentic".into(),
977            };
978            let wit = pack_descriptor_to_wit(desc.clone());
979            let round = match pack_descriptor_from_wit(wit) {
980                Ok(desc) => desc,
981                Err(err) => panic!("pack descriptor rejected: {err}"),
982            };
983            assert_eq!(round, desc);
984        }
985
986        for kind in [
987            types::FlowKind::Messaging,
988            types::FlowKind::Event,
989            types::FlowKind::ComponentConfig,
990            types::FlowKind::Job,
991            types::FlowKind::Http,
992        ] {
993            let desc = FlowDescriptor {
994                id: fixture_id("flow"),
995                kind,
996                tags: vec!["tag".into()],
997                entrypoints: vec!["main".into()],
998            };
999            let wit = flow_descriptor_to_wit(desc.clone());
1000            let round = match flow_descriptor_from_wit(wit) {
1001                Ok(desc) => desc,
1002                Err(err) => panic!("flow descriptor rejected: {err}"),
1003            };
1004            assert_eq!(round, desc);
1005        }
1006    }
1007}