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 = bindings::greentic::interfaces_types::types::TenantCtx;
42type WitImpersonation = bindings::greentic::interfaces_types::types::Impersonation;
43type WitSessionCursor = bindings::greentic::interfaces_types::types::SessionCursor;
44type WitOutcome = bindings::greentic::interfaces_types::types::Outcome;
45type WitOutcomePending = bindings::greentic::interfaces_types::types::OutcomePending;
46type WitOutcomeError = bindings::greentic::interfaces_types::types::OutcomeError;
47type WitErrorCode = bindings::greentic::interfaces_types::types::ErrorCode;
48type WitAllowList = bindings::greentic::interfaces_types::types::AllowList;
49type WitProtocol = bindings::greentic::interfaces_types::types::Protocol;
50type WitNetworkPolicy = bindings::greentic::interfaces_types::types::NetworkPolicy;
51type WitSpanContext = bindings::greentic::interfaces_types::types::SpanContext;
52type WitPackRef = bindings::greentic::interfaces_types::types::PackRef;
53type WitSignature = bindings::greentic::interfaces_types::types::Signature;
54type WitSignatureAlgorithm = bindings::greentic::interfaces_types::types::SignatureAlgorithm;
55
56/// Convert a WIT `TenantCtx` into the shared `greentic_types::TenantCtx`.
57pub fn tenant_ctx_from_wit(ctx: WitTenantCtx) -> MapperResult<types::TenantCtx> {
58    types::TenantCtx::try_from(ctx)
59}
60
61/// Convert a shared `greentic_types::TenantCtx` into the WIT `TenantCtx`.
62pub fn tenant_ctx_to_wit(ctx: types::TenantCtx) -> MapperResult<WitTenantCtx> {
63    WitTenantCtx::try_from(ctx)
64}
65
66impl TryFrom<WitImpersonation> for types::Impersonation {
67    type Error = types::GreenticError;
68
69    fn try_from(value: WitImpersonation) -> MapperResult<Self> {
70        Ok(Self {
71            actor_id: value.actor_id.try_into()?,
72            reason: value.reason,
73        })
74    }
75}
76
77impl From<types::Impersonation> for WitImpersonation {
78    fn from(value: types::Impersonation) -> Self {
79        Self {
80            actor_id: value.actor_id.into(),
81            reason: value.reason,
82        }
83    }
84}
85
86impl TryFrom<WitTenantCtx> for types::TenantCtx {
87    type Error = types::GreenticError;
88
89    fn try_from(value: WitTenantCtx) -> MapperResult<Self> {
90        let WitTenantCtx {
91            env,
92            tenant,
93            tenant_id,
94            team,
95            team_id,
96            user,
97            user_id,
98            trace_id,
99            correlation_id,
100            session_id,
101            flow_id,
102            node_id,
103            provider_id,
104            deadline_ms,
105            attempt,
106            idempotency_key,
107            impersonation,
108            attributes,
109        } = value;
110
111        let deadline =
112            deadline_ms.map(|ms| types::InvocationDeadline::from_unix_millis(ms as i128));
113
114        let env = env.try_into()?;
115        let tenant = tenant.try_into()?;
116        let tenant_id = tenant_id.try_into()?;
117        let team = team.map(|item| item.try_into()).transpose()?;
118        let team_id = team_id.map(|item| item.try_into()).transpose()?;
119        let user = user.map(|item| item.try_into()).transpose()?;
120        let user_id = user_id.map(|item| item.try_into()).transpose()?;
121        let impersonation = impersonation
122            .map(types::Impersonation::try_from)
123            .transpose()?;
124        let attributes: BTreeMap<String, String> = attributes.into_iter().collect();
125
126        Ok(Self {
127            env,
128            tenant,
129            tenant_id,
130            team,
131            team_id,
132            user,
133            user_id,
134            session_id,
135            flow_id,
136            node_id,
137            provider_id,
138            trace_id,
139            correlation_id,
140            attributes,
141            deadline,
142            attempt,
143            idempotency_key,
144            impersonation,
145        })
146    }
147}
148
149impl TryFrom<types::TenantCtx> for WitTenantCtx {
150    type Error = types::GreenticError;
151
152    fn try_from(value: types::TenantCtx) -> MapperResult<Self> {
153        let deadline_ms = match value.deadline {
154            Some(deadline) => Some(i128_to_i64(deadline.unix_millis())?),
155            None => None,
156        };
157        let attributes: Vec<(String, String)> = value.attributes.into_iter().collect();
158
159        Ok(Self {
160            env: value.env.into(),
161            tenant: value.tenant.into(),
162            tenant_id: value.tenant_id.into(),
163            team: value.team.map(Into::into),
164            team_id: value.team_id.map(Into::into),
165            user: value.user.map(Into::into),
166            user_id: value.user_id.map(Into::into),
167            session_id: value.session_id,
168            flow_id: value.flow_id,
169            node_id: value.node_id,
170            provider_id: value.provider_id,
171            trace_id: value.trace_id,
172            correlation_id: value.correlation_id,
173            attributes,
174            deadline_ms,
175            attempt: value.attempt,
176            idempotency_key: value.idempotency_key,
177            impersonation: value.impersonation.map(Into::into),
178        })
179    }
180}
181
182impl From<WitSessionCursor> for types::SessionCursor {
183    fn from(value: WitSessionCursor) -> Self {
184        Self {
185            node_pointer: value.node_pointer,
186            wait_reason: value.wait_reason,
187            outbox_marker: value.outbox_marker,
188        }
189    }
190}
191
192impl From<types::SessionCursor> for WitSessionCursor {
193    fn from(value: types::SessionCursor) -> Self {
194        Self {
195            node_pointer: value.node_pointer,
196            wait_reason: value.wait_reason,
197            outbox_marker: value.outbox_marker,
198        }
199    }
200}
201
202impl From<WitErrorCode> for types::ErrorCode {
203    fn from(value: WitErrorCode) -> Self {
204        match value {
205            WitErrorCode::Unknown => Self::Unknown,
206            WitErrorCode::InvalidInput => Self::InvalidInput,
207            WitErrorCode::NotFound => Self::NotFound,
208            WitErrorCode::Conflict => Self::Conflict,
209            WitErrorCode::Timeout => Self::Timeout,
210            WitErrorCode::Unauthenticated => Self::Unauthenticated,
211            WitErrorCode::PermissionDenied => Self::PermissionDenied,
212            WitErrorCode::RateLimited => Self::RateLimited,
213            WitErrorCode::Unavailable => Self::Unavailable,
214            WitErrorCode::Internal => Self::Internal,
215        }
216    }
217}
218
219impl From<types::ErrorCode> for WitErrorCode {
220    fn from(value: types::ErrorCode) -> Self {
221        match value {
222            types::ErrorCode::Unknown => Self::Unknown,
223            types::ErrorCode::InvalidInput => Self::InvalidInput,
224            types::ErrorCode::NotFound => Self::NotFound,
225            types::ErrorCode::Conflict => Self::Conflict,
226            types::ErrorCode::Timeout => Self::Timeout,
227            types::ErrorCode::Unauthenticated => Self::Unauthenticated,
228            types::ErrorCode::PermissionDenied => Self::PermissionDenied,
229            types::ErrorCode::RateLimited => Self::RateLimited,
230            types::ErrorCode::Unavailable => Self::Unavailable,
231            types::ErrorCode::Internal => Self::Internal,
232        }
233    }
234}
235
236impl From<WitOutcome> for types::Outcome<String> {
237    fn from(value: WitOutcome) -> Self {
238        match value {
239            WitOutcome::Done(val) => Self::Done(val),
240            WitOutcome::Pending(payload) => Self::Pending {
241                reason: payload.reason,
242                expected_input: payload.expected_input,
243            },
244            WitOutcome::Error(payload) => Self::Error {
245                code: payload.code.into(),
246                message: payload.message,
247            },
248        }
249    }
250}
251
252impl From<types::Outcome<String>> for WitOutcome {
253    fn from(value: types::Outcome<String>) -> Self {
254        match value {
255            types::Outcome::Done(val) => Self::Done(val),
256            types::Outcome::Pending {
257                reason,
258                expected_input,
259            } => Self::Pending(WitOutcomePending {
260                reason,
261                expected_input,
262            }),
263            types::Outcome::Error { code, message } => Self::Error(WitOutcomeError {
264                code: code.into(),
265                message,
266            }),
267        }
268    }
269}
270
271impl From<WitProtocol> for types::Protocol {
272    fn from(value: WitProtocol) -> Self {
273        match value {
274            WitProtocol::Http => Self::Http,
275            WitProtocol::Https => Self::Https,
276            WitProtocol::Tcp => Self::Tcp,
277            WitProtocol::Udp => Self::Udp,
278            WitProtocol::Grpc => Self::Grpc,
279            WitProtocol::Custom(v) => Self::Custom(v),
280        }
281    }
282}
283
284impl From<types::Protocol> for WitProtocol {
285    fn from(value: types::Protocol) -> Self {
286        match value {
287            types::Protocol::Http => Self::Http,
288            types::Protocol::Https => Self::Https,
289            types::Protocol::Tcp => Self::Tcp,
290            types::Protocol::Udp => Self::Udp,
291            types::Protocol::Grpc => Self::Grpc,
292            types::Protocol::Custom(v) => Self::Custom(v),
293        }
294    }
295}
296
297impl From<WitAllowList> for types::AllowList {
298    fn from(value: WitAllowList) -> Self {
299        Self {
300            domains: value.domains,
301            ports: value.ports,
302            protocols: value.protocols.into_iter().map(Into::into).collect(),
303        }
304    }
305}
306
307impl From<types::AllowList> for WitAllowList {
308    fn from(value: types::AllowList) -> Self {
309        Self {
310            domains: value.domains,
311            ports: value.ports,
312            protocols: value.protocols.into_iter().map(Into::into).collect(),
313        }
314    }
315}
316
317impl From<WitNetworkPolicy> for types::NetworkPolicy {
318    fn from(value: WitNetworkPolicy) -> Self {
319        Self {
320            egress: value.egress.into(),
321            deny_on_miss: value.deny_on_miss,
322        }
323    }
324}
325
326impl From<types::NetworkPolicy> for WitNetworkPolicy {
327    fn from(value: types::NetworkPolicy) -> Self {
328        Self {
329            egress: value.egress.into(),
330            deny_on_miss: value.deny_on_miss,
331        }
332    }
333}
334
335impl TryFrom<WitSpanContext> for types::SpanContext {
336    type Error = types::GreenticError;
337
338    fn try_from(value: WitSpanContext) -> MapperResult<Self> {
339        let WitSpanContext {
340            tenant,
341            session_id,
342            flow_id,
343            node_id,
344            provider,
345            start_ms,
346            end_ms,
347        } = value;
348
349        let start = start_ms.map(timestamp_ms_to_offset).transpose()?;
350        let end = end_ms.map(timestamp_ms_to_offset).transpose()?;
351        let tenant = tenant.try_into()?;
352
353        Ok(Self {
354            tenant,
355            session_id: session_id.map(types::SessionKey::from),
356            flow_id,
357            node_id,
358            provider,
359            start,
360            end,
361        })
362    }
363}
364
365impl TryFrom<types::SpanContext> for WitSpanContext {
366    type Error = types::GreenticError;
367
368    fn try_from(value: types::SpanContext) -> MapperResult<Self> {
369        let start_ms = value
370            .start
371            .as_ref()
372            .map(offset_to_timestamp_ms)
373            .transpose()?;
374        let end_ms = value.end.as_ref().map(offset_to_timestamp_ms).transpose()?;
375
376        Ok(Self {
377            tenant: value.tenant.into(),
378            session_id: value.session_id.map(|key| key.to_string()),
379            flow_id: value.flow_id,
380            node_id: value.node_id,
381            provider: value.provider,
382            start_ms,
383            end_ms,
384        })
385    }
386}
387
388impl From<WitSignatureAlgorithm> for types::SignatureAlgorithm {
389    fn from(value: WitSignatureAlgorithm) -> Self {
390        match value {
391            WitSignatureAlgorithm::Ed25519 => Self::Ed25519,
392            WitSignatureAlgorithm::Other(v) => Self::Other(v),
393        }
394    }
395}
396
397impl From<types::SignatureAlgorithm> for WitSignatureAlgorithm {
398    fn from(value: types::SignatureAlgorithm) -> Self {
399        match value {
400            types::SignatureAlgorithm::Ed25519 => Self::Ed25519,
401            types::SignatureAlgorithm::Other(v) => Self::Other(v),
402        }
403    }
404}
405
406impl From<WitSignature> for types::Signature {
407    fn from(value: WitSignature) -> Self {
408        Self {
409            key_id: value.key_id,
410            algorithm: value.algorithm.into(),
411            signature: value.signature,
412        }
413    }
414}
415
416impl From<types::Signature> for WitSignature {
417    fn from(value: types::Signature) -> Self {
418        Self {
419            key_id: value.key_id,
420            algorithm: value.algorithm.into(),
421            signature: value.signature,
422        }
423    }
424}
425
426impl TryFrom<WitPackRef> for types::PackRef {
427    type Error = types::GreenticError;
428
429    fn try_from(value: WitPackRef) -> MapperResult<Self> {
430        let version = Version::parse(&value.version)
431            .map_err(|err| invalid_input(format!("invalid version: {err}")))?;
432        Ok(Self {
433            oci_url: value.oci_url,
434            version,
435            digest: value.digest,
436            signatures: value.signatures.into_iter().map(Into::into).collect(),
437        })
438    }
439}
440
441impl From<types::PackRef> for WitPackRef {
442    fn from(value: types::PackRef) -> Self {
443        Self {
444            oci_url: value.oci_url,
445            version: value.version.to_string(),
446            digest: value.digest,
447            signatures: value.signatures.into_iter().map(Into::into).collect(),
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use std::convert::TryFrom;
456
457    fn fixture_id<T>(value: &str) -> T
458    where
459        T: TryFrom<String, Error = types::GreenticError>,
460    {
461        T::try_from(value.to_owned())
462            .unwrap_or_else(|err| panic!("invalid fixture identifier '{value}': {err}"))
463    }
464
465    fn sample_tenant_ctx() -> types::TenantCtx {
466        types::TenantCtx {
467            env: fixture_id("prod"),
468            tenant: fixture_id("tenant-1"),
469            tenant_id: fixture_id("tenant-1"),
470            team: Some(fixture_id("team-42")),
471            team_id: Some(fixture_id("team-42")),
472            user: Some(fixture_id("user-7")),
473            user_id: Some(fixture_id("user-7")),
474            attributes: BTreeMap::new(),
475            session_id: Some("sess-42".into()),
476            flow_id: Some("flow-42".into()),
477            node_id: Some("node-42".into()),
478            provider_id: Some("provider-42".into()),
479            trace_id: Some("trace".into()),
480            correlation_id: Some("corr".into()),
481            deadline: Some(types::InvocationDeadline::from_unix_millis(
482                1_700_000_000_000,
483            )),
484            attempt: 2,
485            idempotency_key: Some("idem".into()),
486            impersonation: Some(types::Impersonation {
487                actor_id: fixture_id("actor"),
488                reason: Some("maintenance".into()),
489            }),
490        }
491    }
492
493    #[test]
494    fn tenant_ctx_roundtrip() {
495        let ctx = sample_tenant_ctx();
496        let wit = match WitTenantCtx::try_from(ctx.clone()) {
497            Ok(value) => value,
498            Err(err) => panic!("failed to map to wit: {err}"),
499        };
500        let back = match types::TenantCtx::try_from(wit) {
501            Ok(value) => value,
502            Err(err) => panic!("failed to map from wit: {err}"),
503        };
504        assert_eq!(back.env.as_str(), ctx.env.as_str());
505        assert_eq!(back.tenant.as_str(), ctx.tenant.as_str());
506        assert!(back.impersonation.is_some());
507        assert!(ctx.impersonation.is_some());
508        assert_eq!(
509            back.impersonation.as_ref().map(|imp| imp.actor_id.as_str()),
510            ctx.impersonation.as_ref().map(|imp| imp.actor_id.as_str())
511        );
512        assert_eq!(back.session_id, ctx.session_id);
513        assert_eq!(back.flow_id, ctx.flow_id);
514        assert_eq!(back.node_id, ctx.node_id);
515        assert_eq!(back.provider_id, ctx.provider_id);
516    }
517
518    #[test]
519    fn outcome_roundtrip() {
520        let pending = types::Outcome::Pending {
521            reason: "waiting".into(),
522            expected_input: Some(vec!["input".into()]),
523        };
524        let wit = WitOutcome::from(pending.clone());
525        let back = types::Outcome::from(wit);
526        match back {
527            types::Outcome::Pending { reason, .. } => {
528                assert_eq!(reason, "waiting");
529            }
530            _ => panic!("expected pending"),
531        }
532    }
533}