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