Skip to main content

greentic_distributor_client/
wit_client.rs

1use crate::{ArtifactLocation, CacheInfo, SignatureSummary};
2use crate::{ComponentDigest, ComponentStatus};
3use crate::{
4    DistributorClient, DistributorEnvironmentId, DistributorError, PackStatusResponse,
5    ResolveComponentRequest, ResolveComponentResponse, SecretFormat, SecretKey, SecretRequirement,
6    SecretScope, TenantCtx,
7};
8use anyhow::anyhow;
9use async_trait::async_trait;
10use greentic_interfaces_guest::distributor_api as wit;
11#[cfg(target_arch = "wasm32")]
12use greentic_interfaces_guest::distributor_api::DistributorApiImports;
13use serde_json::Value;
14
15#[async_trait]
16pub trait DistributorApiBindings: Send + Sync {
17    async fn resolve_component(
18        &self,
19        req: wit::ResolveComponentRequest,
20    ) -> Result<wit::ResolveComponentResponse, anyhow::Error>;
21
22    async fn get_pack_status(
23        &self,
24        tenant_id: &str,
25        environment_id: &str,
26        pack_id: &str,
27    ) -> Result<String, anyhow::Error>;
28
29    async fn get_pack_status_v2(
30        &self,
31        tenant_id: &str,
32        environment_id: &str,
33        pack_id: &str,
34    ) -> Result<wit::PackStatusResponse, anyhow::Error>;
35
36    async fn warm_pack(
37        &self,
38        tenant_id: &str,
39        environment_id: &str,
40        pack_id: &str,
41    ) -> Result<(), anyhow::Error>;
42}
43
44#[derive(Clone)]
45pub struct WitDistributorClient<B: DistributorApiBindings> {
46    bindings: B,
47}
48
49impl<B: DistributorApiBindings> WitDistributorClient<B> {
50    pub fn new(bindings: B) -> Self {
51        Self { bindings }
52    }
53}
54
55/// Default bindings that call the generated distributor-api imports when running as a WASM guest.
56#[derive(Clone, Default)]
57pub struct GeneratedDistributorApiBindings;
58
59#[async_trait]
60impl DistributorApiBindings for GeneratedDistributorApiBindings {
61    async fn resolve_component(
62        &self,
63        req: wit::ResolveComponentRequest,
64    ) -> Result<wit::ResolveComponentResponse, anyhow::Error> {
65        #[cfg(target_arch = "wasm32")]
66        {
67            let api = DistributorApiImports::new();
68            Ok(api.resolve_component(&req))
69        }
70        #[cfg(not(target_arch = "wasm32"))]
71        {
72            let _ = req;
73            Err(anyhow!(
74                "distributor-api imports are only available on wasm32 targets"
75            ))
76        }
77    }
78
79    async fn get_pack_status(
80        &self,
81        tenant_id: &str,
82        environment_id: &str,
83        pack_id: &str,
84    ) -> Result<String, anyhow::Error> {
85        #[cfg(target_arch = "wasm32")]
86        {
87            let api = DistributorApiImports::new();
88            Ok(api.get_pack_status(
89                &tenant_id.to_string(),
90                &environment_id.to_string(),
91                &pack_id.to_string(),
92            ))
93        }
94        #[cfg(not(target_arch = "wasm32"))]
95        {
96            let _ = (tenant_id, environment_id, pack_id);
97            Err(anyhow!(
98                "distributor-api imports are only available on wasm32 targets"
99            ))
100        }
101    }
102
103    async fn get_pack_status_v2(
104        &self,
105        tenant_id: &str,
106        environment_id: &str,
107        pack_id: &str,
108    ) -> Result<wit::PackStatusResponse, anyhow::Error> {
109        #[cfg(target_arch = "wasm32")]
110        {
111            let api = DistributorApiImports::new();
112            Ok(api.get_pack_status_v2(
113                &tenant_id.to_string(),
114                &environment_id.to_string(),
115                &pack_id.to_string(),
116            ))
117        }
118        #[cfg(not(target_arch = "wasm32"))]
119        {
120            let _ = (tenant_id, environment_id, pack_id);
121            Err(anyhow!(
122                "distributor-api imports are only available on wasm32 targets"
123            ))
124        }
125    }
126
127    async fn warm_pack(
128        &self,
129        tenant_id: &str,
130        environment_id: &str,
131        pack_id: &str,
132    ) -> Result<(), anyhow::Error> {
133        #[cfg(target_arch = "wasm32")]
134        {
135            let api = DistributorApiImports::new();
136            api.warm_pack(
137                &tenant_id.to_string(),
138                &environment_id.to_string(),
139                &pack_id.to_string(),
140            );
141            Ok(())
142        }
143        #[cfg(not(target_arch = "wasm32"))]
144        {
145            let _ = (tenant_id, environment_id, pack_id);
146            Err(anyhow!(
147                "distributor-api imports are only available on wasm32 targets"
148            ))
149        }
150    }
151}
152
153#[async_trait]
154impl<B> DistributorClient for WitDistributorClient<B>
155where
156    B: DistributorApiBindings,
157{
158    async fn resolve_component(
159        &self,
160        req: ResolveComponentRequest,
161    ) -> Result<ResolveComponentResponse, DistributorError> {
162        let wit_req = to_wit_request(req)?;
163        let resp = self
164            .bindings
165            .resolve_component(wit_req)
166            .await
167            .map_err(|e| {
168                // TODO: once distributor-api exposes structured errors, map to
169                // NotFound/PermissionDenied instead of a generic Wit error.
170                DistributorError::Wit(e.to_string())
171            })?;
172        from_wit_response(resp)
173    }
174
175    async fn get_pack_status(
176        &self,
177        tenant: &TenantCtx,
178        env: &DistributorEnvironmentId,
179        pack_id: &str,
180    ) -> Result<Value, DistributorError> {
181        let payload = self
182            .bindings
183            .get_pack_status(tenant.tenant_id.as_str(), env.as_str(), pack_id)
184            .await
185            .map_err(|e| DistributorError::Wit(e.to_string()))?;
186        serde_json::from_str(&payload).map_err(DistributorError::Serde)
187    }
188
189    async fn get_pack_status_v2(
190        &self,
191        tenant: &TenantCtx,
192        env: &DistributorEnvironmentId,
193        pack_id: &str,
194    ) -> Result<PackStatusResponse, DistributorError> {
195        let payload = self
196            .bindings
197            .get_pack_status_v2(tenant.tenant_id.as_str(), env.as_str(), pack_id)
198            .await
199            .map_err(|e| DistributorError::Wit(e.to_string()))?;
200        from_wit_pack_status(payload)
201    }
202
203    async fn warm_pack(
204        &self,
205        tenant: &TenantCtx,
206        env: &DistributorEnvironmentId,
207        pack_id: &str,
208    ) -> Result<(), DistributorError> {
209        self.bindings
210            .warm_pack(tenant.tenant_id.as_str(), env.as_str(), pack_id)
211            .await
212            .map_err(|e| DistributorError::Wit(e.to_string()))
213    }
214}
215
216fn to_wit_request(
217    req: ResolveComponentRequest,
218) -> Result<wit::ResolveComponentRequest, DistributorError> {
219    Ok(wit::ResolveComponentRequest {
220        tenant_id: req.tenant.tenant_id.to_string(),
221        environment_id: req.environment_id.as_str().to_string(),
222        pack_id: req.pack_id,
223        component_id: req.component_id,
224        version: req.version,
225        extra: serde_json::to_string(&req.extra)?,
226    })
227}
228
229fn from_wit_response(
230    resp: wit::ResolveComponentResponse,
231) -> Result<ResolveComponentResponse, DistributorError> {
232    let status = match resp.component_status {
233        wit::ComponentStatus::Pending => ComponentStatus::Pending,
234        wit::ComponentStatus::Ready => ComponentStatus::Ready,
235        wit::ComponentStatus::Failed => ComponentStatus::Failed {
236            reason: "failed".to_string(),
237        },
238    };
239    let artifact = match resp.artifact_location.kind.as_str() {
240        "file" | "file_path" => ArtifactLocation::FilePath {
241            path: resp.artifact_location.value,
242        },
243        "oci" | "oci_reference" => ArtifactLocation::OciReference {
244            reference: resp.artifact_location.value,
245        },
246        _ => ArtifactLocation::DistributorInternal {
247            handle: resp.artifact_location.value,
248        },
249    };
250    let signature = SignatureSummary {
251        verified: resp.signature_summary.verified,
252        signer: resp.signature_summary.signer,
253        extra: serde_json::from_str(&resp.signature_summary.extra)?,
254    };
255    let cache = CacheInfo {
256        size_bytes: resp.cache_info.size_bytes,
257        last_used_utc: resp.cache_info.last_used_utc,
258        last_refreshed_utc: resp.cache_info.last_refreshed_utc,
259    };
260    Ok(ResolveComponentResponse {
261        status,
262        digest: ComponentDigest(resp.digest),
263        artifact,
264        signature,
265        cache,
266        secret_requirements: from_wit_secret_requirements(resp.secret_requirements)?,
267    })
268}
269
270fn from_wit_pack_status(
271    resp: wit::PackStatusResponse,
272) -> Result<PackStatusResponse, DistributorError> {
273    Ok(PackStatusResponse {
274        status: resp.status,
275        secret_requirements: from_wit_secret_requirements(resp.secret_requirements)?,
276        extra: serde_json::from_str(&resp.extra)?,
277    })
278}
279
280fn from_wit_secret_requirements(
281    reqs: impl WitSecretRequirementsExt,
282) -> Result<Option<Vec<SecretRequirement>>, DistributorError> {
283    reqs.into_optional_vec()
284        .map(|requirements| {
285            requirements
286                .into_iter()
287                .map(from_wit_secret_requirement)
288                .collect()
289        })
290        .transpose()
291}
292
293trait WitSecretRequirementsExt {
294    fn into_optional_vec(self) -> Option<Vec<wit::SecretRequirement>>;
295}
296
297impl WitSecretRequirementsExt for Vec<wit::SecretRequirement> {
298    fn into_optional_vec(self) -> Option<Vec<wit::SecretRequirement>> {
299        Some(self)
300    }
301}
302
303impl WitSecretRequirementsExt for Option<Vec<wit::SecretRequirement>> {
304    fn into_optional_vec(self) -> Option<Vec<wit::SecretRequirement>> {
305        self
306    }
307}
308
309fn from_wit_secret_requirement(
310    req: wit::SecretRequirement,
311) -> Result<SecretRequirement, DistributorError> {
312    let key = SecretKey::parse(&req.key).map_err(|e| {
313        DistributorError::InvalidResponse(format!("invalid secret key `{}`: {e}", req.key))
314    })?;
315    let scope = req.scope.map(|scope| SecretScope {
316        env: scope.env,
317        tenant: scope.tenant,
318        team: scope.team,
319    });
320    let format = req.format.map(|format| match format as u8 {
321        0 => SecretFormat::Bytes,
322        1 => SecretFormat::Text,
323        2 => SecretFormat::Json,
324        _ => unreachable!("unexpected WIT secret format discriminant"),
325    });
326    let schema = match req.schema {
327        Some(schema) => Some(serde_json::from_str(&schema)?),
328        None => None,
329    };
330    let mut requirement = SecretRequirement::default();
331    requirement.key = key;
332    requirement.required = req.required;
333    requirement.description = req.description;
334    requirement.scope = scope;
335    requirement.format = format;
336    requirement.schema = schema;
337    requirement.examples = req.examples;
338    Ok(requirement)
339}