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