Skip to main content

hot_dev/
resources.rs

1//! Resource namespaces mirroring the Hot API v1 resources. This module is a
2//! port of hot-python's resources.py.
3
4use std::collections::HashSet;
5use std::sync::Arc;
6use std::time::Duration;
7
8use futures_util::StreamExt;
9use reqwest::header::{ACCEPT, CONTENT_TYPE};
10use reqwest::multipart::{Form, Part};
11use reqwest::{Method, Response};
12use serde::Serialize;
13use serde_json::{json, Value};
14
15use crate::streaming::{
16    extract_run_result, iter_stream, wait_for_call_result, wait_for_run, wait_for_run_result,
17    wait_for_task, EventStream, StreamEventExt,
18};
19use crate::transport::{data_object, decode_data_response, enc, enc_path, Transport};
20use crate::{Error, JsonObject, Result};
21
22/// Optional parameters for [`StreamsResource::subscribe_with_event`].
23pub struct SubscribeWithEventOptions {
24    /// Scopes the subscription to a project id or slug.
25    pub project: Option<String>,
26    /// Automatically resubscribe across the API's 5-minute SSE timeout.
27    pub reconnect: bool,
28    /// Caps reconnection attempts.
29    pub max_attempts: u32,
30}
31
32impl Default for SubscribeWithEventOptions {
33    fn default() -> Self {
34        SubscribeWithEventOptions {
35            project: None,
36            reconnect: true,
37            max_attempts: 5,
38        }
39    }
40}
41
42/// Optional parameters for [`StreamsResource::wait_for_run_result`].
43pub struct WaitOptions<'a> {
44    /// Bounds the wait.
45    pub timeout: Duration,
46    /// Receives streamed text chunks from the matched run.
47    pub on_chunk: Option<&'a mut (dyn FnMut(&str) + Send)>,
48}
49
50impl Default for WaitOptions<'_> {
51    fn default() -> Self {
52        WaitOptions {
53            timeout: Duration::from_secs(60),
54            on_chunk: None,
55        }
56    }
57}
58
59/// Optional parameters for [`TasksResource::wait`].
60pub struct TaskWaitOptions {
61    /// Bounds the wait.
62    pub timeout: Duration,
63    /// Caps reconnection attempts.
64    pub max_attempts: u32,
65}
66
67/// Optional parameters for [`RunsResource::wait`].
68pub struct RunWaitOptions {
69    /// Bounds the wait.
70    pub timeout: Duration,
71    /// Caps reconnection attempts.
72    pub max_attempts: u32,
73}
74
75impl Default for RunWaitOptions {
76    fn default() -> Self {
77        RunWaitOptions {
78            timeout: Duration::from_secs(60),
79            max_attempts: 5,
80        }
81    }
82}
83
84impl Default for TaskWaitOptions {
85    fn default() -> Self {
86        TaskWaitOptions {
87            timeout: Duration::from_secs(60),
88            max_attempts: 5,
89        }
90    }
91}
92
93/// Optional parameters for [`EventsResource::call_hot`].
94pub struct CallOptions<'a> {
95    /// Bounds the wait for the run result.
96    pub timeout: Duration,
97    /// Receives streamed text chunks from the run.
98    pub on_chunk: Option<&'a mut (dyn FnMut(&str) + Send)>,
99}
100
101impl Default for CallOptions<'_> {
102    fn default() -> Self {
103        CallOptions {
104            timeout: Duration::from_secs(60),
105            on_chunk: None,
106        }
107    }
108}
109
110/// A build archive to upload.
111pub struct BuildUpload {
112    /// The build archive content.
113    pub content: Vec<u8>,
114    /// Names the multipart file part. Defaults to "build".
115    pub filename: Option<String>,
116    /// The build content hash.
117    pub hash: String,
118    /// Optionally pins the build id.
119    pub build_id: Option<String>,
120}
121
122macro_rules! resource {
123    ($(#[$doc:meta])* $name:ident) => {
124        $(#[$doc])*
125        pub struct $name {
126            transport: Arc<Transport>,
127        }
128
129        impl $name {
130            pub(crate) fn new(transport: Arc<Transport>) -> Self {
131                Self { transport }
132            }
133        }
134    };
135}
136
137resource!(
138    /// Publishes and inspects Hot events.
139    EventsResource
140);
141resource!(
142    /// Lists and inspects runs.
143    RunsResource
144);
145resource!(
146    /// Reads environment info and environment events.
147    EnvResource
148);
149resource!(
150    /// Reports organization usage and limits.
151    OrgResource
152);
153resource!(
154    /// Subscribes to Hot run streams.
155    StreamsResource
156);
157resource!(
158    /// Reads and waits for durable asynchronous tasks.
159    TasksResource
160);
161resource!(
162    /// Uploads, downloads, lists, and deletes files.
163    FilesResource
164);
165resource!(
166    /// Manages projects.
167    ProjectsResource
168);
169resource!(
170    /// Uploads, deploys, and inspects builds.
171    BuildsResource
172);
173resource!(
174    /// Manages encrypted project context variables.
175    ContextResource
176);
177resource!(
178    /// Manages custom domains.
179    DomainsResource
180);
181resource!(
182    /// Creates and revokes scoped sessions.
183    SessionsResource
184);
185resource!(
186    /// Creates and revokes scoped service keys.
187    ServiceKeysResource
188);
189
190impl EventsResource {
191    /// Publishes an event and returns the created event data
192    /// (event_id, stream_id, ...).
193    pub async fn publish(&self, body: impl Serialize) -> Result<JsonObject> {
194        let envelope = self
195            .transport
196            .request_json(
197                Method::POST,
198                "/events",
199                Some(&serde_json::to_value(body)?),
200                &[],
201            )
202            .await?;
203        Ok(data_object(envelope))
204    }
205
206    /// Publishes a hot:call event for `fn_name` with `args` and waits for the
207    /// run result, unwrapping a `{"$ok": ...}` result value.
208    pub async fn call_hot(
209        &self,
210        fn_name: &str,
211        args: Vec<Value>,
212        opts: CallOptions<'_>,
213    ) -> Result<Value> {
214        let body = json!({
215            "event_type": "hot:call",
216            "event_data": { "fn": fn_name, "args": args },
217        });
218        let Value::Object(body) = body else {
219            unreachable!()
220        };
221        let run =
222            wait_for_call_result(self.transport.clone(), body, opts.timeout, opts.on_chunk).await?;
223        Ok(extract_run_result(run.get("result").cloned()))
224    }
225
226    /// Returns the event list envelope (data + pagination).
227    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
228        self.transport
229            .request_json(Method::GET, "/events", None, query)
230            .await
231    }
232
233    /// Returns a single event.
234    pub async fn get(&self, event_id: &str) -> Result<JsonObject> {
235        let path = format!("/events/{}", enc(event_id));
236        let envelope = self
237            .transport
238            .request_json(Method::GET, &path, None, &[])
239            .await?;
240        Ok(data_object(envelope))
241    }
242
243    /// Returns the runs triggered by an event.
244    pub async fn get_runs(&self, event_id: &str) -> Result<JsonObject> {
245        let path = format!("/events/{}/runs", enc(event_id));
246        self.transport
247            .request_json(Method::GET, &path, None, &[])
248            .await
249    }
250}
251
252impl RunsResource {
253    /// Returns the run list envelope (data + pagination).
254    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
255        self.transport
256            .request_json(Method::GET, "/runs", None, query)
257            .await
258    }
259
260    /// Returns aggregate run statistics.
261    pub async fn stats(&self) -> Result<JsonObject> {
262        let envelope = self
263            .transport
264            .request_json(Method::GET, "/runs/stats", None, &[])
265            .await?;
266        Ok(data_object(envelope))
267    }
268
269    /// Returns a single run.
270    pub async fn get(&self, run_id: &str) -> Result<JsonObject> {
271        let path = format!("/runs/{}", enc(run_id));
272        let envelope = self
273            .transport
274            .request_json(Method::GET, &path, None, &[])
275            .await?;
276        Ok(data_object(envelope))
277    }
278
279    /// Streams durable run:update snapshots until the run is terminal.
280    pub fn subscribe(&self, run_id: &str) -> EventStream {
281        iter_stream(
282            self.transport.clone(),
283            Method::GET,
284            format!("/runs/{}/subscribe", enc(run_id)),
285            None,
286            Vec::new(),
287        )
288    }
289
290    /// Waits for successful run completion. Failed and cancelled runs return
291    /// [`Error::RunWaitFailed`] with the final durable snapshot.
292    pub async fn wait(&self, run_id: &str, opts: RunWaitOptions) -> Result<JsonObject> {
293        wait_for_run(
294            self.transport.clone(),
295            run_id,
296            opts.timeout,
297            opts.max_attempts,
298        )
299        .await
300    }
301}
302
303impl EnvResource {
304    /// Returns environment info.
305    pub async fn get(&self) -> Result<JsonObject> {
306        let envelope = self
307            .transport
308            .request_json(Method::GET, "/env", None, &[])
309            .await?;
310        Ok(data_object(envelope))
311    }
312
313    /// Streams environment events. Requires API key credentials and a live
314    /// API pub/sub backend.
315    pub fn subscribe(&self) -> EventStream {
316        iter_stream(
317            self.transport.clone(),
318            Method::GET,
319            "/env/subscribe".to_string(),
320            None,
321            Vec::new(),
322        )
323    }
324}
325
326impl OrgResource {
327    /// Returns organization usage and limits.
328    pub async fn usage(&self) -> Result<JsonObject> {
329        let envelope = self
330            .transport
331            .request_json(Method::GET, "/org/usage", None, &[])
332            .await?;
333        Ok(data_object(envelope))
334    }
335}
336
337impl StreamsResource {
338    /// Streams events from an existing run stream.
339    pub fn subscribe(&self, stream_id: &str, project: Option<&str>) -> EventStream {
340        iter_stream(
341            self.transport.clone(),
342            Method::GET,
343            format!("/streams/{}/subscribe", enc(stream_id)),
344            None,
345            project_query(project),
346        )
347    }
348
349    /// Streams events from an existing run stream via POST.
350    pub fn subscribe_post(&self, stream_id: &str, project: Option<&str>) -> EventStream {
351        iter_stream(
352            self.transport.clone(),
353            Method::POST,
354            format!("/streams/{}/subscribe", enc(stream_id)),
355            None,
356            project_query(project),
357        )
358    }
359
360    /// Subscribes to a stream and returns the run object once the run created
361    /// by `event_id` reaches run:stop. run:fail and run:cancel become
362    /// [`Error::RunFailed`] carrying the run's result message.
363    pub async fn wait_for_run_result(
364        &self,
365        stream_id: &str,
366        event_id: &str,
367        opts: WaitOptions<'_>,
368    ) -> Result<JsonObject> {
369        wait_for_run_result(
370            self.transport.clone(),
371            stream_id,
372            event_id,
373            opts.timeout,
374            opts.on_chunk,
375        )
376        .await
377    }
378
379    /// Atomically subscribes then publishes an event, yielding stream events
380    /// until the run completes.
381    ///
382    /// The Hot API closes SSE subscriptions after 5 minutes of idle time.
383    /// While `opts.reconnect` is true (the default), the SDK resubscribes to
384    /// the same stream_id and keeps yielding events until a terminal
385    /// run:stop/run:fail/run:cancel arrives. Replayed run:start and terminal
386    /// events are deduped by run_id; stream:data chunks are not replayed and
387    /// may be lost across the disconnect window.
388    pub fn subscribe_with_event(
389        &self,
390        body: impl Serialize,
391        opts: SubscribeWithEventOptions,
392    ) -> EventStream {
393        let body = match serde_json::to_value(body) {
394            Ok(body) => body,
395            Err(error) => {
396                return Box::pin(async_stream::stream! { yield Err(Error::Json(error)) });
397            }
398        };
399        let transport = self.transport.clone();
400        let query = project_query(opts.project.as_deref());
401        let once = move |transport: Arc<Transport>, query: Vec<(String, String)>| {
402            iter_stream(
403                transport,
404                Method::POST,
405                "/streams/subscribe-with-event".to_string(),
406                Some(body.clone()),
407                query,
408            )
409        };
410
411        if !opts.reconnect {
412            return once(transport, query);
413        }
414
415        Box::pin(async_stream::stream! {
416            let mut seen_start: HashSet<String> = HashSet::new();
417            let mut seen_terminal: HashSet<String> = HashSet::new();
418            let mut stream_id: Option<String> = None;
419            let mut event_id: Option<String> = None;
420            let mut attempts: u32 = 0;
421
422            loop {
423                let mut source = match &stream_id {
424                    None => once(transport.clone(), query.clone()),
425                    Some(id) => iter_stream(
426                        transport.clone(),
427                        Method::GET,
428                        format!("/streams/{}/subscribe", enc(id)),
429                        None,
430                        query.clone(),
431                    ),
432                };
433
434                let mut terminal = false;
435                while let Some(item) = source.next().await {
436                    let event = match item {
437                        Ok(event) => event,
438                        Err(error) => {
439                            if stream_id.is_none() || attempts >= opts.max_attempts {
440                                yield Err(error);
441                                return;
442                            }
443                            break;
444                        }
445                    };
446
447                    match event.event_type() {
448                        "event:published" => {
449                            if let Some(published) =
450                                event.get("stream_id").and_then(Value::as_str)
451                            {
452                                stream_id = Some(published.to_string());
453                            }
454                            if let Some(published) =
455                                event.get("event_id").and_then(Value::as_str)
456                            {
457                                event_id = Some(published.to_string());
458                            }
459                        }
460                        "run:start" => {
461                            let run_id = event.run_id().map(str::to_string);
462                            if let Some(run_id) = run_id {
463                                if !seen_start.insert(run_id) {
464                                    continue;
465                                }
466                            }
467                        }
468                        "run:stop" | "run:fail" | "run:cancel" => {
469                            let run_id = event.run_id().map(str::to_string);
470                            if let Some(run_id) = run_id {
471                                if !seen_terminal.insert(run_id) {
472                                    continue;
473                                }
474                            }
475                            terminal = event_id.as_deref().is_some_and(|published| {
476                                crate::streaming::event_id_of_run(event.run()) == Some(published)
477                            });
478                        }
479                        _ => {}
480                    }
481
482                    yield Ok(event);
483                    if terminal {
484                        return;
485                    }
486                }
487
488                if terminal {
489                    return;
490                }
491                if stream_id.is_none() {
492                    yield Err(Error::Protocol(
493                        "stream ended before event:published was received".to_string(),
494                    ));
495                    return;
496                }
497                if attempts >= opts.max_attempts {
498                    yield Err(Error::Protocol(
499                        "stream ended before the published event's run completed".to_string(),
500                    ));
501                    return;
502                }
503
504                attempts += 1;
505                let delay = Duration::from_millis((250 * u64::from(attempts)).min(2000));
506                tokio::time::sleep(delay).await;
507            }
508        })
509    }
510}
511
512impl TasksResource {
513    /// Returns the latest durable task snapshot.
514    pub async fn get(&self, task_id: &str) -> Result<JsonObject> {
515        let path = format!("/tasks/{}", enc(task_id));
516        let envelope = self
517            .transport
518            .request_json(Method::GET, &path, None, &[])
519            .await?;
520        Ok(data_object(envelope))
521    }
522
523    /// Streams durable task:update snapshots until the task is terminal.
524    pub fn subscribe(&self, task_id: &str) -> EventStream {
525        iter_stream(
526            self.transport.clone(),
527            Method::GET,
528            format!("/tasks/{}/subscribe", enc(task_id)),
529            None,
530            Vec::new(),
531        )
532    }
533
534    /// Waits for successful task completion. Failed, cancelled, and timed-out
535    /// tasks return [`Error::TaskFailed`] with the final durable snapshot.
536    pub async fn wait(&self, task_id: &str, opts: TaskWaitOptions) -> Result<JsonObject> {
537        wait_for_task(
538            self.transport.clone(),
539            task_id,
540            opts.timeout,
541            opts.max_attempts,
542        )
543        .await
544    }
545}
546
547impl FilesResource {
548    /// Returns the file list envelope (data + pagination).
549    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
550        self.transport
551            .request_json(Method::GET, "/files", None, query)
552            .await
553    }
554
555    /// Returns a single file's metadata.
556    pub async fn get(&self, file_id: &str) -> Result<JsonObject> {
557        let path = format!("/files/{}", enc(file_id));
558        let envelope = self
559            .transport
560            .request_json(Method::GET, &path, None, &[])
561            .await?;
562        Ok(data_object(envelope))
563    }
564
565    /// Deletes a file.
566    pub async fn delete(&self, file_id: &str) -> Result<()> {
567        let path = format!("/files/{}", enc(file_id));
568        let builder = self.transport.request_builder(Method::DELETE, &path);
569        self.transport.execute(builder).await?;
570        Ok(())
571    }
572
573    /// Returns the raw HTTP response streaming the file content.
574    pub async fn download(&self, file_id: &str) -> Result<Response> {
575        let path = format!("/files/{}/download", enc(file_id));
576        let builder = self.transport.request_builder(Method::GET, &path);
577        self.transport.execute(builder).await
578    }
579
580    /// Uploads content to a slash-separated file path.
581    pub async fn upload(
582        &self,
583        path: &str,
584        content: Vec<u8>,
585        content_type: Option<&str>,
586    ) -> Result<JsonObject> {
587        let request_path = format!("/files/upload/{}", enc_path(path));
588        let mut builder = self
589            .transport
590            .request_builder(Method::PUT, &request_path)
591            .header(ACCEPT, "application/json")
592            .body(content);
593        if let Some(content_type) = content_type {
594            builder = builder.header(CONTENT_TYPE, content_type);
595        }
596        let response = self.transport.execute(builder).await?;
597        decode_data_response(response).await
598    }
599
600    /// Starts a multipart upload.
601    pub async fn initiate_upload(&self, body: impl Serialize) -> Result<JsonObject> {
602        let envelope = self
603            .transport
604            .request_json(
605                Method::POST,
606                "/files/uploads",
607                Some(&serde_json::to_value(body)?),
608                &[],
609            )
610            .await?;
611        Ok(data_object(envelope))
612    }
613
614    /// Uploads one part of a multipart upload.
615    pub async fn upload_part(
616        &self,
617        upload_id: &str,
618        part_number: u32,
619        content: Vec<u8>,
620    ) -> Result<JsonObject> {
621        let path = format!("/files/uploads/{}/{}", enc(upload_id), part_number);
622        let builder = self
623            .transport
624            .request_builder(Method::PUT, &path)
625            .header(ACCEPT, "application/json")
626            .body(content);
627        let response = self.transport.execute(builder).await?;
628        decode_data_response(response).await
629    }
630
631    /// Finishes a multipart upload.
632    pub async fn complete_upload(&self, upload_id: &str) -> Result<JsonObject> {
633        let path = format!("/files/uploads/{}/complete", enc(upload_id));
634        let envelope = self
635            .transport
636            .request_json(Method::POST, &path, None, &[])
637            .await?;
638        Ok(data_object(envelope))
639    }
640
641    /// Cancels a multipart upload.
642    pub async fn abort_upload(&self, upload_id: &str) -> Result<()> {
643        let path = format!("/files/uploads/{}", enc(upload_id));
644        let builder = self.transport.request_builder(Method::DELETE, &path);
645        self.transport.execute(builder).await?;
646        Ok(())
647    }
648}
649
650impl ProjectsResource {
651    /// Returns the project list envelope (data + pagination).
652    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
653        self.transport
654            .request_json(Method::GET, "/projects", None, query)
655            .await
656    }
657
658    /// Creates a project.
659    pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
660        let envelope = self
661            .transport
662            .request_json(
663                Method::POST,
664                "/projects",
665                Some(&serde_json::to_value(body)?),
666                &[],
667            )
668            .await?;
669        Ok(data_object(envelope))
670    }
671
672    /// Returns a project by id or slug.
673    pub async fn get(&self, project: &str) -> Result<JsonObject> {
674        let path = format!("/projects/{}", enc(project));
675        let envelope = self
676            .transport
677            .request_json(Method::GET, &path, None, &[])
678            .await?;
679        Ok(data_object(envelope))
680    }
681
682    /// Patches a project.
683    pub async fn update(&self, project: &str, body: impl Serialize) -> Result<JsonObject> {
684        let path = format!("/projects/{}", enc(project));
685        let envelope = self
686            .transport
687            .request_json(
688                Method::PATCH,
689                &path,
690                Some(&serde_json::to_value(body)?),
691                &[],
692            )
693            .await?;
694        Ok(data_object(envelope))
695    }
696
697    /// Deletes a project.
698    pub async fn delete(&self, project: &str) -> Result<()> {
699        let path = format!("/projects/{}", enc(project));
700        let builder = self.transport.request_builder(Method::DELETE, &path);
701        self.transport.execute(builder).await?;
702        Ok(())
703    }
704
705    /// Activates a project.
706    pub async fn activate(&self, project: &str) -> Result<JsonObject> {
707        let path = format!("/projects/{}/activate", enc(project));
708        let envelope = self
709            .transport
710            .request_json(Method::POST, &path, None, &[])
711            .await?;
712        Ok(data_object(envelope))
713    }
714
715    /// Deactivates a project.
716    pub async fn deactivate(&self, project: &str) -> Result<JsonObject> {
717        let path = format!("/projects/{}/deactivate", enc(project));
718        let envelope = self
719            .transport
720            .request_json(Method::POST, &path, None, &[])
721            .await?;
722        Ok(data_object(envelope))
723    }
724
725    /// Returns the project's event handlers.
726    pub async fn event_handlers(&self, project: &str) -> Result<JsonObject> {
727        let path = format!("/projects/{}/event-handlers", enc(project));
728        self.transport
729            .request_json(Method::GET, &path, None, &[])
730            .await
731    }
732
733    /// Returns the project's schedules.
734    pub async fn schedules(&self, project: &str) -> Result<JsonObject> {
735        let path = format!("/projects/{}/schedules", enc(project));
736        self.transport
737            .request_json(Method::GET, &path, None, &[])
738            .await
739    }
740}
741
742impl BuildsResource {
743    /// Returns the build list envelope across all projects.
744    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
745        self.transport
746            .request_json(Method::GET, "/builds", None, query)
747            .await
748    }
749
750    /// Returns the build list envelope for one project.
751    pub async fn list_for_project(
752        &self,
753        project: &str,
754        query: &[(&str, &str)],
755    ) -> Result<JsonObject> {
756        let path = format!("/projects/{}/builds", enc(project));
757        self.transport
758            .request_json(Method::GET, &path, None, query)
759            .await
760    }
761
762    /// Returns a single build.
763    pub async fn get(&self, project: &str, build_id: &str) -> Result<JsonObject> {
764        let path = format!("/projects/{}/builds/{}", enc(project), enc(build_id));
765        let envelope = self
766            .transport
767            .request_json(Method::GET, &path, None, &[])
768            .await?;
769        Ok(data_object(envelope))
770    }
771
772    /// Returns the project's deployed build.
773    pub async fn deployed(&self, project: &str) -> Result<JsonObject> {
774        let path = format!("/projects/{}/builds/deployed", enc(project));
775        let envelope = self
776            .transport
777            .request_json(Method::GET, &path, None, &[])
778            .await?;
779        Ok(data_object(envelope))
780    }
781
782    /// Returns the project's live build.
783    pub async fn live(&self, project: &str) -> Result<JsonObject> {
784        let path = format!("/projects/{}/builds/live", enc(project));
785        let envelope = self
786            .transport
787            .request_json(Method::GET, &path, None, &[])
788            .await?;
789        Ok(data_object(envelope))
790    }
791
792    /// Uploads a build archive as multipart form data.
793    pub async fn upload(&self, project: &str, upload: BuildUpload) -> Result<JsonObject> {
794        let mut form = Form::new().text("hash", upload.hash);
795        if let Some(build_id) = upload.build_id {
796            form = form.text("build_id", build_id);
797        }
798        let part = Part::bytes(upload.content)
799            .file_name(upload.filename.unwrap_or_else(|| "build".to_string()));
800        form = form.part("file", part);
801
802        let path = format!("/projects/{}/builds", enc(project));
803        let builder = self
804            .transport
805            .request_builder(Method::POST, &path)
806            .header(ACCEPT, "application/json")
807            .multipart(form);
808        let response = self.transport.execute(builder).await?;
809        decode_data_response(response).await
810    }
811
812    /// Returns the raw HTTP response streaming the build archive.
813    pub async fn download(&self, project: &str, build_id: &str) -> Result<Response> {
814        let path = format!(
815            "/projects/{}/builds/{}/download",
816            enc(project),
817            enc(build_id)
818        );
819        let builder = self.transport.request_builder(Method::GET, &path);
820        self.transport.execute(builder).await
821    }
822
823    /// Deploys a build.
824    pub async fn deploy(&self, project: &str, build_id: &str) -> Result<JsonObject> {
825        let path = format!("/projects/{}/builds/{}/deploy", enc(project), enc(build_id));
826        let envelope = self
827            .transport
828            .request_json(Method::POST, &path, None, &[])
829            .await?;
830        Ok(data_object(envelope))
831    }
832}
833
834impl ContextResource {
835    /// Returns the context variable list envelope for a project.
836    pub async fn list(&self, project: &str) -> Result<JsonObject> {
837        let path = format!("/projects/{}/context", enc(project));
838        self.transport
839            .request_json(Method::GET, &path, None, &[])
840            .await
841    }
842
843    /// Creates a context variable.
844    pub async fn create(&self, project: &str, body: impl Serialize) -> Result<JsonObject> {
845        let path = format!("/projects/{}/context", enc(project));
846        let envelope = self
847            .transport
848            .request_json(Method::POST, &path, Some(&serde_json::to_value(body)?), &[])
849            .await?;
850        Ok(data_object(envelope))
851    }
852
853    /// Replaces a context variable.
854    pub async fn update(
855        &self,
856        project: &str,
857        key: &str,
858        body: impl Serialize,
859    ) -> Result<JsonObject> {
860        let path = format!("/projects/{}/context/{}", enc(project), enc(key));
861        let envelope = self
862            .transport
863            .request_json(Method::PUT, &path, Some(&serde_json::to_value(body)?), &[])
864            .await?;
865        Ok(data_object(envelope))
866    }
867
868    /// Deletes a context variable.
869    pub async fn delete(&self, project: &str, key: &str) -> Result<()> {
870        let path = format!("/projects/{}/context/{}", enc(project), enc(key));
871        let builder = self.transport.request_builder(Method::DELETE, &path);
872        self.transport.execute(builder).await?;
873        Ok(())
874    }
875}
876
877impl DomainsResource {
878    /// Registers a custom domain.
879    pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
880        let envelope = self
881            .transport
882            .request_json(
883                Method::POST,
884                "/domains",
885                Some(&serde_json::to_value(body)?),
886                &[],
887            )
888            .await?;
889        Ok(data_object(envelope))
890    }
891
892    /// Returns the domain list envelope.
893    pub async fn list(&self) -> Result<JsonObject> {
894        self.transport
895            .request_json(Method::GET, "/domains", None, &[])
896            .await
897    }
898
899    /// Returns a single domain.
900    pub async fn get(&self, domain_id: &str) -> Result<JsonObject> {
901        let path = format!("/domains/{}", enc(domain_id));
902        let envelope = self
903            .transport
904            .request_json(Method::GET, &path, None, &[])
905            .await?;
906        Ok(data_object(envelope))
907    }
908
909    /// Deletes a domain.
910    pub async fn delete(&self, domain_id: &str) -> Result<()> {
911        let path = format!("/domains/{}", enc(domain_id));
912        let builder = self.transport.request_builder(Method::DELETE, &path);
913        self.transport.execute(builder).await?;
914        Ok(())
915    }
916
917    /// Triggers domain verification.
918    pub async fn verify(&self, domain_id: &str) -> Result<JsonObject> {
919        let path = format!("/domains/{}/verify", enc(domain_id));
920        let envelope = self
921            .transport
922            .request_json(Method::POST, &path, None, &[])
923            .await?;
924        Ok(data_object(envelope))
925    }
926}
927
928impl SessionsResource {
929    /// Creates a scoped session.
930    pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
931        let envelope = self
932            .transport
933            .request_json(
934                Method::POST,
935                "/sessions",
936                Some(&serde_json::to_value(body)?),
937                &[],
938            )
939            .await?;
940        Ok(data_object(envelope))
941    }
942
943    /// Returns the session list envelope.
944    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
945        self.transport
946            .request_json(Method::GET, "/sessions", None, query)
947            .await
948    }
949
950    /// Revokes one session.
951    pub async fn revoke(&self, session_id: &str) -> Result<()> {
952        let path = format!("/sessions/{}", enc(session_id));
953        let builder = self.transport.request_builder(Method::DELETE, &path);
954        self.transport.execute(builder).await?;
955        Ok(())
956    }
957
958    /// Revokes every session.
959    pub async fn revoke_all(&self) -> Result<JsonObject> {
960        let envelope = self
961            .transport
962            .request_json(Method::DELETE, "/sessions", None, &[])
963            .await?;
964        Ok(data_object(envelope))
965    }
966}
967
968impl ServiceKeysResource {
969    /// Creates a scoped service key.
970    pub async fn create(&self, body: impl Serialize) -> Result<JsonObject> {
971        let envelope = self
972            .transport
973            .request_json(
974                Method::POST,
975                "/service-keys",
976                Some(&serde_json::to_value(body)?),
977                &[],
978            )
979            .await?;
980        Ok(data_object(envelope))
981    }
982
983    /// Returns the service key list envelope.
984    pub async fn list(&self, query: &[(&str, &str)]) -> Result<JsonObject> {
985        self.transport
986            .request_json(Method::GET, "/service-keys", None, query)
987            .await
988    }
989
990    /// Returns a single service key.
991    pub async fn get(&self, service_key_id: &str) -> Result<JsonObject> {
992        let path = format!("/service-keys/{}", enc(service_key_id));
993        let envelope = self
994            .transport
995            .request_json(Method::GET, &path, None, &[])
996            .await?;
997        Ok(data_object(envelope))
998    }
999
1000    /// Patches a service key.
1001    pub async fn update(&self, service_key_id: &str, body: impl Serialize) -> Result<JsonObject> {
1002        let path = format!("/service-keys/{}", enc(service_key_id));
1003        let envelope = self
1004            .transport
1005            .request_json(
1006                Method::PATCH,
1007                &path,
1008                Some(&serde_json::to_value(body)?),
1009                &[],
1010            )
1011            .await?;
1012        Ok(data_object(envelope))
1013    }
1014
1015    /// Revokes one service key.
1016    pub async fn revoke(&self, service_key_id: &str) -> Result<()> {
1017        let path = format!("/service-keys/{}", enc(service_key_id));
1018        let builder = self.transport.request_builder(Method::DELETE, &path);
1019        self.transport.execute(builder).await?;
1020        Ok(())
1021    }
1022
1023    /// Revokes every service key.
1024    pub async fn revoke_all(&self) -> Result<JsonObject> {
1025        let envelope = self
1026            .transport
1027            .request_json(Method::DELETE, "/service-keys", None, &[])
1028            .await?;
1029        Ok(data_object(envelope))
1030    }
1031}
1032
1033fn project_query(project: Option<&str>) -> Vec<(String, String)> {
1034    match project {
1035        Some(project) => vec![("project".to_string(), project.to_string())],
1036        None => Vec::new(),
1037    }
1038}