Skip to main content

cloudconvert_sdk/
jobs.rs

1use std::{collections::BTreeMap, convert::identity, fmt};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::tasks::{
7    ArchiveTask, AzureBlobExportTask, AzureBlobImportTask, Base64ImportTask, CaptureWebsiteTask,
8    CommandTask, ConvertTask, ExportUploadTask, ExportUrlTask, GoogleCloudStorageExportTask,
9    GoogleCloudStorageImportTask, ImportUploadTask, ImportUrlTask, Input, MergeTask, MetadataTask,
10    MetadataWriteTask, OpenStackExportTask, OpenStackImportTask, OptimizeTask, PdfATask,
11    PdfDecryptTask, PdfEncryptTask, PdfExtractPagesTask, PdfOcrTask, PdfRotatePagesTask,
12    PdfSplitPagesTask, PdfXTask, RawImportTask, S3ExportTask, S3ImportTask, SftpExportTask,
13    SftpImportTask, TaskRequest, ThumbnailTask, WatermarkTask,
14};
15
16fn redacted_option<T>(value: &Option<T>) -> Option<&'static str> {
17    value.as_ref().map(|_| "REDACTED")
18}
19
20struct RedactedValueMap<'a>(&'a BTreeMap<String, Value>);
21
22impl fmt::Debug for RedactedValueMap<'_> {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        let mut debug = f.debug_map();
25        for key in self.0.keys() {
26            debug.entry(key, &"REDACTED");
27        }
28        debug.finish()
29    }
30}
31
32/// Rate limit information extracted from CloudConvert response headers.
33#[derive(Clone, Debug, Default, Deserialize, Serialize)]
34#[non_exhaustive]
35pub struct RateLimit {
36    #[serde(default)]
37    pub limit: Option<u64>,
38    #[serde(default)]
39    pub remaining: Option<u64>,
40    #[serde(default)]
41    pub reset: Option<u64>,
42    #[serde(default)]
43    pub retry_after: Option<u64>,
44}
45
46/// Pagination links returned by CloudConvert list endpoints.
47#[derive(Clone, Debug, Default, Deserialize, Serialize)]
48#[non_exhaustive]
49pub struct PaginationLinks {
50    #[serde(default)]
51    pub first: Option<String>,
52    #[serde(default)]
53    pub last: Option<String>,
54    #[serde(default)]
55    pub prev: Option<String>,
56    #[serde(default)]
57    pub next: Option<String>,
58    #[serde(flatten)]
59    pub extra: BTreeMap<String, Value>,
60}
61
62/// Pagination metadata returned by CloudConvert list endpoints.
63#[derive(Clone, Debug, Default, Deserialize, Serialize)]
64#[non_exhaustive]
65pub struct PaginationMeta {
66    #[serde(default)]
67    pub current_page: Option<u32>,
68    #[serde(default)]
69    pub from: Option<u32>,
70    #[serde(default)]
71    pub path: Option<String>,
72    #[serde(default)]
73    pub per_page: Option<u32>,
74    #[serde(default)]
75    pub to: Option<u32>,
76    #[serde(default)]
77    pub total: Option<u32>,
78    #[serde(default)]
79    pub last_page: Option<u32>,
80    #[serde(flatten)]
81    pub extra: BTreeMap<String, Value>,
82}
83
84/// A paginated API response.
85#[derive(Clone, Debug, Default, Serialize)]
86#[non_exhaustive]
87pub struct Page<T> {
88    pub data: Vec<T>,
89    pub links: PaginationLinks,
90    pub meta: PaginationMeta,
91    #[serde(skip)]
92    pub rate_limit: Option<RateLimit>,
93}
94
95/// A non-paginated API response that preserves envelope metadata.
96#[derive(Clone, Debug, Serialize)]
97#[non_exhaustive]
98pub struct ApiResponse<T> {
99    pub data: T,
100    pub links: PaginationLinks,
101    pub meta: PaginationMeta,
102    #[serde(skip)]
103    pub rate_limit: Option<RateLimit>,
104}
105
106/// Request body for `POST /v2/jobs`.
107///
108/// Use [`JobCreateRequest::linear`] for serial pipelines and
109/// [`JobCreateRequest::graph`] for branches or joins. Both builders generate
110/// the task names CloudConvert requires.
111///
112/// ```
113/// use cloudconvert_sdk::{FileExtension, JobCreateRequest};
114///
115/// let request = JobCreateRequest::linear()
116///     .import_url("https://example.test/input.docx")
117///     .convert(FileExtension::Pdf)
118///     .export_url()
119///     .build();
120///
121/// let payload = serde_json::to_value(request).unwrap();
122/// assert_eq!(payload["tasks"]["convert"]["input"], "import-url");
123/// ```
124#[derive(Clone, Default, Serialize)]
125pub struct JobCreateRequest {
126    tasks: BTreeMap<String, TaskRequest>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    tag: Option<String>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    webhook_url: Option<String>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub(crate) redirect: Option<bool>,
133    #[serde(flatten, skip_serializing_if = "BTreeMap::is_empty")]
134    extra: BTreeMap<String, Value>,
135}
136
137/// Name assigned to a task in a CloudConvert job request.
138///
139/// `TaskName` is the serialized task-map key. Pass handles returned by
140/// [`JobGraphBuilder`] methods as dependency inputs for later graph tasks.
141#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
142pub struct TaskName(String);
143
144impl TaskName {
145    fn new(value: impl Into<String>) -> Self {
146        Self(value.into())
147    }
148
149    /// Returns the task name string used in the serialized job payload.
150    pub fn as_str(&self) -> &str {
151        &self.0
152    }
153}
154
155impl AsRef<str> for TaskName {
156    fn as_ref(&self) -> &str {
157        self.as_str()
158    }
159}
160
161impl fmt::Display for TaskName {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        formatter.write_str(self.as_str())
164    }
165}
166
167impl From<TaskName> for String {
168    fn from(value: TaskName) -> Self {
169        value.0
170    }
171}
172
173impl From<&TaskName> for String {
174    fn from(value: &TaskName) -> Self {
175        value.as_str().to_string()
176    }
177}
178
179impl From<TaskName> for Input {
180    fn from(value: TaskName) -> Self {
181        Self::from(value.0)
182    }
183}
184
185impl From<&TaskName> for Input {
186    fn from(value: &TaskName) -> Self {
187        Self::from(value.as_str())
188    }
189}
190
191impl From<Vec<TaskName>> for Input {
192    fn from(value: Vec<TaskName>) -> Self {
193        Self::Tasks(value.into_iter().map(String::from).collect())
194    }
195}
196
197impl From<Vec<&TaskName>> for Input {
198    fn from(value: Vec<&TaskName>) -> Self {
199        Self::Tasks(value.into_iter().map(String::from).collect())
200    }
201}
202
203impl<const N: usize> From<[TaskName; N]> for Input {
204    fn from(value: [TaskName; N]) -> Self {
205        Self::Tasks(value.into_iter().map(String::from).collect())
206    }
207}
208
209impl<const N: usize> From<[&TaskName; N]> for Input {
210    fn from(value: [&TaskName; N]) -> Self {
211        Self::Tasks(value.into_iter().map(String::from).collect())
212    }
213}
214
215impl JobCreateRequest {
216    /// Starts a general-purpose job builder.
217    ///
218    /// Prefer [`JobCreateRequest::linear`] for serial pipelines and
219    /// [`JobCreateRequest::graph`] for branch or join jobs.
220    pub fn builder() -> JobBuilder {
221        JobBuilder::default()
222    }
223
224    /// Starts a job builder for a serial task pipeline.
225    pub fn linear() -> JobBuilder {
226        Self::builder()
227    }
228
229    /// Builds a job graph with task handles scoped to a closure.
230    ///
231    /// Each graph method returns a [`TaskName`] that can be passed as the input
232    /// for later tasks. The returned [`JobBuilder`] can still be used to set
233    /// top-level job options before calling [`JobBuilder::build`].
234    ///
235    /// ```
236    /// use cloudconvert_sdk::{FileExtension, JobCreateRequest};
237    ///
238    /// let request = JobCreateRequest::graph(|job| {
239    ///     let source = job.import_url("https://example.test/input.docx");
240    ///     let pdf = job.convert(&source, FileExtension::Pdf);
241    ///     let png = job.convert(&source, FileExtension::Png);
242    ///     job.export_url([&pdf, &png]);
243    /// })
244    /// .tag("branch-demo")
245    /// .build();
246    ///
247    /// let payload = serde_json::to_value(request).unwrap();
248    /// assert_eq!(payload["tasks"]["convert"]["input"], "import-url");
249    /// assert_eq!(payload["tasks"]["convert-2"]["input"], "import-url");
250    /// ```
251    pub fn graph(configure: impl FnOnce(&mut JobGraphBuilder)) -> JobBuilder {
252        let mut graph = JobGraphBuilder::default();
253        configure(&mut graph);
254        graph.into_builder()
255    }
256
257    /// Returns the keyed task map that will be serialized as `tasks`.
258    pub fn tasks(&self) -> &BTreeMap<String, TaskRequest> {
259        &self.tasks
260    }
261
262    /// Returns the optional job tag.
263    pub fn tag(&self) -> Option<&str> {
264        self.tag.as_deref()
265    }
266
267    /// Returns the optional webhook URL.
268    pub fn webhook_url(&self) -> Option<&str> {
269        self.webhook_url.as_deref()
270    }
271
272    /// Returns whether CloudConvert should use redirect responses for the job.
273    pub fn redirect(&self) -> Option<bool> {
274        self.redirect
275    }
276
277    /// Returns additional job fields set through [`JobBuilder::option`].
278    pub fn extra(&self) -> &BTreeMap<String, Value> {
279        &self.extra
280    }
281}
282
283impl fmt::Debug for JobCreateRequest {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        f.debug_struct("JobCreateRequest")
286            .field("tasks", &self.tasks)
287            .field("tag", &self.tag)
288            .field("webhook_url", &self.webhook_url)
289            .field("redirect", &self.redirect)
290            .field("extra", &RedactedValueMap(&self.extra))
291            .finish()
292    }
293}
294
295#[derive(Clone, Debug, Default)]
296/// Builder for serial [`JobCreateRequest`] pipelines.
297///
298/// For linear jobs, use the fluent task shorthands and let the SDK wire each
299/// task to the previous task.
300///
301/// ```
302/// use cloudconvert_sdk::{FileExtension, JobCreateRequest};
303///
304/// let request = JobCreateRequest::linear()
305///     .import_url("https://example.test/input.docx")
306///     .convert(FileExtension::Pdf)
307///     .export_url()
308///     .build();
309///
310/// let payload = serde_json::to_value(request).unwrap();
311/// assert_eq!(payload["tasks"]["export-url"]["input"], "convert");
312/// ```
313///
314/// Use `*_with(...)` methods when a task needs options. Use
315/// [`JobCreateRequest::graph`] when a later task needs a handle to a specific
316/// earlier task.
317pub struct JobBuilder {
318    request: JobCreateRequest,
319    last_task: Option<TaskName>,
320}
321
322macro_rules! linear_pdf_task_methods {
323    ($method:ident, $with_method:ident, $task_type:ident) => {
324        /// Appends a PDF operation task using the previous task as input.
325        pub fn $method(self) -> Self {
326            let input = self.previous_input();
327            self.append_task(TaskRequest::$method(input))
328        }
329
330        /// Appends and configures a PDF operation task using the previous task as input.
331        pub fn $with_method<F>(self, configure: F) -> Self
332        where
333            F: FnOnce($task_type) -> $task_type,
334        {
335            let input = self.previous_input();
336            self.append_configured_task($task_type::new(input), configure)
337        }
338    };
339}
340
341macro_rules! graph_pdf_task_methods {
342    ($method:ident, $with_method:ident, $task_type:ident) => {
343        /// Adds a PDF operation task.
344        pub fn $method(&mut self, input: impl Into<Input>) -> TaskName {
345            self.$with_method(input, identity)
346        }
347
348        /// Adds and configures a PDF operation task.
349        pub fn $with_method<F>(&mut self, input: impl Into<Input>, configure: F) -> TaskName
350        where
351            F: FnOnce($task_type) -> $task_type,
352        {
353            self.add_configured_task($task_type::new(input), configure)
354        }
355    };
356}
357
358impl JobBuilder {
359    /// Creates an empty job builder.
360    pub fn new() -> Self {
361        Self::default()
362    }
363
364    /// Sets an optional job tag.
365    pub fn tag(mut self, tag: impl Into<String>) -> Self {
366        self.request.tag = Some(tag.into());
367        self
368    }
369
370    /// Sets the webhook URL CloudConvert should call for this job.
371    pub fn webhook_url(mut self, webhook_url: impl Into<String>) -> Self {
372        self.request.webhook_url = Some(webhook_url.into());
373        self
374    }
375
376    /// Sets whether CloudConvert should redirect on synchronous job completion.
377    pub fn redirect(mut self, redirect: bool) -> Self {
378        self.request.redirect = Some(redirect);
379        self
380    }
381
382    /// Adds a custom top-level job field.
383    pub fn option(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
384        self.request.extra.insert(key.into(), value.into());
385        self
386    }
387
388    /// Adds a task with an explicit CloudConvert task name.
389    ///
390    /// This keeps compatibility with CloudConvert's keyed task object and with
391    /// existing code that already depends on task names.
392    pub fn task(mut self, name: impl Into<String>, task: impl Into<TaskRequest>) -> Self {
393        let name = TaskName::new(name);
394        self.request
395            .tasks
396            .insert(name.as_str().to_string(), task.into());
397        self.last_task = Some(name);
398        self
399    }
400
401    /// Adds a task with a generated name and returns that name as a handle.
402    ///
403    /// Generated names are derived from the task operation, such as
404    /// `import-url`, `convert`, or `export-url`. Duplicate operation names get
405    /// numeric suffixes.
406    ///
407    /// ```
408    /// use cloudconvert_sdk::{ConvertTask, FileExtension, JobCreateRequest, TaskRequest};
409    ///
410    /// let mut builder = JobCreateRequest::builder();
411    /// let import = builder.add_task(TaskRequest::import_url("https://example.test/input.docx"));
412    /// let convert = builder.add_task(ConvertTask::new(&import, FileExtension::Pdf));
413    ///
414    /// assert_eq!(import.as_str(), "import-url");
415    /// assert_eq!(convert.as_str(), "convert");
416    /// ```
417    pub fn add_task(&mut self, task: impl Into<TaskRequest>) -> TaskName {
418        let task = task.into();
419        let name = generated_task_name(task.operation(), &self.request.tasks);
420        self.request.tasks.insert(name.as_str().to_string(), task);
421        self.last_task = Some(name.clone());
422        name
423    }
424
425    /// Adds a task with an explicit name and returns that name as a handle.
426    pub fn add_named_task(
427        &mut self,
428        name: impl Into<String>,
429        task: impl Into<TaskRequest>,
430    ) -> TaskName {
431        let name = TaskName::new(name);
432        self.request
433            .tasks
434            .insert(name.as_str().to_string(), task.into());
435        self.last_task = Some(name.clone());
436        name
437    }
438
439    /// Appends an `import/url` task.
440    pub fn import_url(self, url: impl Into<String>) -> Self {
441        self.append_task(TaskRequest::import_url(url))
442    }
443
444    /// Appends an `import/upload` task.
445    pub fn import_upload(self) -> Self {
446        self.append_task(TaskRequest::import_upload())
447    }
448
449    /// Appends an `import/base64` task.
450    pub fn import_base64(self, file: impl Into<String>, filename: impl Into<String>) -> Self {
451        self.append_task(TaskRequest::import_base64(file, filename))
452    }
453
454    /// Appends an `import/raw` task.
455    pub fn import_raw(self, file: impl Into<String>, filename: impl Into<String>) -> Self {
456        self.append_task(TaskRequest::import_raw(file, filename))
457    }
458
459    /// Appends an `import/s3` task.
460    pub fn import_s3(
461        self,
462        bucket: impl Into<String>,
463        region: impl Into<String>,
464        access_key_id: impl Into<String>,
465        secret_access_key: impl Into<String>,
466    ) -> Self {
467        self.append_task(TaskRequest::import_s3(
468            bucket,
469            region,
470            access_key_id,
471            secret_access_key,
472        ))
473    }
474
475    /// Appends an `import/azure/blob` task.
476    pub fn import_azure_blob(
477        self,
478        storage_account: impl Into<String>,
479        container: impl Into<String>,
480    ) -> Self {
481        self.append_task(TaskRequest::import_azure_blob(storage_account, container))
482    }
483
484    /// Appends an `import/google-cloud-storage` task.
485    pub fn import_google_cloud_storage(
486        self,
487        project_id: impl Into<String>,
488        bucket: impl Into<String>,
489        client_email: impl Into<String>,
490        private_key: impl Into<String>,
491    ) -> Self {
492        self.append_task(TaskRequest::import_google_cloud_storage(
493            project_id,
494            bucket,
495            client_email,
496            private_key,
497        ))
498    }
499
500    /// Appends an `import/openstack` task.
501    pub fn import_openstack(
502        self,
503        auth_url: impl Into<String>,
504        username: impl Into<String>,
505        password: impl Into<String>,
506        region: impl Into<String>,
507        container: impl Into<String>,
508    ) -> Self {
509        self.append_task(TaskRequest::import_openstack(
510            auth_url, username, password, region, container,
511        ))
512    }
513
514    /// Appends an `import/sftp` task.
515    pub fn import_sftp(self, host: impl Into<String>, username: impl Into<String>) -> Self {
516        self.append_task(TaskRequest::import_sftp(host, username))
517    }
518
519    /// Appends a `convert` task using the previous task as input.
520    pub fn convert(self, output_format: impl Into<String>) -> Self {
521        let input = self.previous_input();
522        self.append_task(TaskRequest::convert(input, output_format))
523    }
524
525    /// Appends a `convert` task with an explicit input format.
526    pub fn convert_with_input_format(
527        self,
528        input_format: impl Into<String>,
529        output_format: impl Into<String>,
530    ) -> Self {
531        let input = self.previous_input();
532        self.append_task(ConvertTask::new(input, output_format).input_format(input_format))
533    }
534
535    /// Appends an `optimize` task using the previous task as input.
536    pub fn optimize(self) -> Self {
537        let input = self.previous_input();
538        self.append_task(TaskRequest::optimize(input))
539    }
540
541    /// Appends a text `watermark` task using the previous task as input.
542    pub fn watermark_text(self, text: impl Into<String>) -> Self {
543        let input = self.previous_input();
544        self.append_task(TaskRequest::watermark(crate::tasks::WatermarkTask::text(
545            input, text,
546        )))
547    }
548
549    /// Appends an image `watermark` task using the previous task as input.
550    pub fn watermark_image(self, image_task_name: impl Into<String>) -> Self {
551        let input = self.previous_input();
552        self.append_task(TaskRequest::watermark(crate::tasks::WatermarkTask::image(
553            input,
554            image_task_name,
555        )))
556    }
557
558    /// Appends a `capture-website` task.
559    pub fn capture_website(self, url: impl Into<String>, output_format: impl Into<String>) -> Self {
560        self.append_task(TaskRequest::capture_website(url, output_format))
561    }
562
563    /// Appends a `thumbnail` task using the previous task as input.
564    pub fn thumbnail(self, output_format: impl Into<String>) -> Self {
565        let input = self.previous_input();
566        self.append_task(TaskRequest::thumbnail(input, output_format))
567    }
568
569    /// Appends a `metadata` task using the previous task as input.
570    pub fn metadata(self) -> Self {
571        let input = self.previous_input();
572        self.append_task(TaskRequest::metadata(input))
573    }
574
575    /// Appends a `metadata/write` task using the previous task as input.
576    pub fn metadata_write(self) -> Self {
577        let input = self.previous_input();
578        self.append_task(TaskRequest::metadata_write(input))
579    }
580
581    /// Appends a `merge` task using the previous task as input.
582    pub fn merge(self, output_format: impl Into<String>) -> Self {
583        let input = self.previous_input();
584        self.append_task(TaskRequest::merge(input, output_format))
585    }
586
587    /// Appends an `archive` task using the previous task as input.
588    pub fn archive(self, output_format: impl Into<String>) -> Self {
589        let input = self.previous_input();
590        self.append_task(TaskRequest::archive(input, output_format))
591    }
592
593    /// Appends a `command` task using the previous task as input.
594    pub fn command(
595        self,
596        engine: impl Into<String>,
597        command: impl Into<String>,
598        arguments: impl Into<String>,
599    ) -> Self {
600        let input = self.previous_input();
601        self.append_task(TaskRequest::command(input, engine, command, arguments))
602    }
603
604    linear_pdf_task_methods!(pdf_a, pdf_a_with, PdfATask);
605    linear_pdf_task_methods!(pdf_x, pdf_x_with, PdfXTask);
606    linear_pdf_task_methods!(pdf_ocr, pdf_ocr_with, PdfOcrTask);
607    linear_pdf_task_methods!(pdf_encrypt, pdf_encrypt_with, PdfEncryptTask);
608    linear_pdf_task_methods!(pdf_decrypt, pdf_decrypt_with, PdfDecryptTask);
609    linear_pdf_task_methods!(pdf_split_pages, pdf_split_pages_with, PdfSplitPagesTask);
610    linear_pdf_task_methods!(
611        pdf_extract_pages,
612        pdf_extract_pages_with,
613        PdfExtractPagesTask
614    );
615    linear_pdf_task_methods!(pdf_rotate_pages, pdf_rotate_pages_with, PdfRotatePagesTask);
616
617    /// Appends an `export/url` task using the previous task as input.
618    pub fn export_url(self) -> Self {
619        let input = self.previous_input();
620        self.append_task(TaskRequest::export_url(input))
621    }
622
623    /// Appends an `export/s3` task using the previous task as input.
624    pub fn export_s3(
625        self,
626        bucket: impl Into<String>,
627        region: impl Into<String>,
628        access_key_id: impl Into<String>,
629        secret_access_key: impl Into<String>,
630    ) -> Self {
631        let input = self.previous_input();
632        self.append_task(TaskRequest::export_s3(
633            input,
634            bucket,
635            region,
636            access_key_id,
637            secret_access_key,
638        ))
639    }
640
641    /// Appends an `export/azure/blob` task using the previous task as input.
642    pub fn export_azure_blob(
643        self,
644        storage_account: impl Into<String>,
645        container: impl Into<String>,
646    ) -> Self {
647        let input = self.previous_input();
648        self.append_task(TaskRequest::export_azure_blob(
649            input,
650            storage_account,
651            container,
652        ))
653    }
654
655    /// Appends an `export/google-cloud-storage` task using the previous task as input.
656    pub fn export_google_cloud_storage(
657        self,
658        project_id: impl Into<String>,
659        bucket: impl Into<String>,
660        client_email: impl Into<String>,
661        private_key: impl Into<String>,
662    ) -> Self {
663        let input = self.previous_input();
664        self.append_task(TaskRequest::export_google_cloud_storage(
665            input,
666            project_id,
667            bucket,
668            client_email,
669            private_key,
670        ))
671    }
672
673    /// Appends an `export/openstack` task using the previous task as input.
674    pub fn export_openstack(
675        self,
676        auth_url: impl Into<String>,
677        username: impl Into<String>,
678        password: impl Into<String>,
679        region: impl Into<String>,
680        container: impl Into<String>,
681    ) -> Self {
682        let input = self.previous_input();
683        self.append_task(TaskRequest::export_openstack(
684            input, auth_url, username, password, region, container,
685        ))
686    }
687
688    /// Appends an `export/sftp` task using the previous task as input.
689    pub fn export_sftp(self, host: impl Into<String>, username: impl Into<String>) -> Self {
690        let input = self.previous_input();
691        self.append_task(TaskRequest::export_sftp(input, host, username))
692    }
693
694    /// Appends an `export/upload` task using the previous task as input.
695    pub fn export_upload(self, url: impl Into<String>) -> Self {
696        let input = self.previous_input();
697        self.append_task(TaskRequest::export_upload(input, url))
698    }
699
700    /// Appends and configures an `import/url` task.
701    pub fn import_url_with<F>(self, url: impl Into<String>, configure: F) -> Self
702    where
703        F: FnOnce(ImportUrlTask) -> ImportUrlTask,
704    {
705        self.append_configured_task(ImportUrlTask::new(url), configure)
706    }
707
708    /// Appends and configures an `import/upload` task.
709    pub fn import_upload_with<F>(self, configure: F) -> Self
710    where
711        F: FnOnce(ImportUploadTask) -> ImportUploadTask,
712    {
713        self.append_configured_task(ImportUploadTask::default(), configure)
714    }
715
716    /// Appends and configures an `import/s3` task.
717    pub fn import_s3_with<F>(
718        self,
719        bucket: impl Into<String>,
720        region: impl Into<String>,
721        access_key_id: impl Into<String>,
722        secret_access_key: impl Into<String>,
723        configure: F,
724    ) -> Self
725    where
726        F: FnOnce(S3ImportTask) -> S3ImportTask,
727    {
728        self.append_configured_task(
729            S3ImportTask::new(bucket, region, access_key_id, secret_access_key),
730            configure,
731        )
732    }
733
734    /// Appends and configures an `import/azure/blob` task.
735    pub fn import_azure_blob_with<F>(
736        self,
737        storage_account: impl Into<String>,
738        container: impl Into<String>,
739        configure: F,
740    ) -> Self
741    where
742        F: FnOnce(AzureBlobImportTask) -> AzureBlobImportTask,
743    {
744        self.append_configured_task(
745            AzureBlobImportTask::new(storage_account, container),
746            configure,
747        )
748    }
749
750    /// Appends and configures an `import/google-cloud-storage` task.
751    pub fn import_google_cloud_storage_with<F>(
752        self,
753        project_id: impl Into<String>,
754        bucket: impl Into<String>,
755        client_email: impl Into<String>,
756        private_key: impl Into<String>,
757        configure: F,
758    ) -> Self
759    where
760        F: FnOnce(GoogleCloudStorageImportTask) -> GoogleCloudStorageImportTask,
761    {
762        self.append_configured_task(
763            GoogleCloudStorageImportTask::new(project_id, bucket, client_email, private_key),
764            configure,
765        )
766    }
767
768    /// Appends and configures an `import/openstack` task.
769    pub fn import_openstack_with<F>(
770        self,
771        auth_url: impl Into<String>,
772        username: impl Into<String>,
773        password: impl Into<String>,
774        region: impl Into<String>,
775        container: impl Into<String>,
776        configure: F,
777    ) -> Self
778    where
779        F: FnOnce(OpenStackImportTask) -> OpenStackImportTask,
780    {
781        self.append_configured_task(
782            OpenStackImportTask::new(auth_url, username, password, region, container),
783            configure,
784        )
785    }
786
787    /// Appends and configures an `import/sftp` task.
788    pub fn import_sftp_with<F>(
789        self,
790        host: impl Into<String>,
791        username: impl Into<String>,
792        configure: F,
793    ) -> Self
794    where
795        F: FnOnce(SftpImportTask) -> SftpImportTask,
796    {
797        self.append_configured_task(SftpImportTask::new(host, username), configure)
798    }
799
800    /// Appends and configures a `convert` task using the previous task as input.
801    pub fn convert_with<F>(self, output_format: impl Into<String>, configure: F) -> Self
802    where
803        F: FnOnce(ConvertTask) -> ConvertTask,
804    {
805        let input = self.previous_input();
806        self.append_configured_task(ConvertTask::new(input, output_format), configure)
807    }
808
809    /// Appends and configures an `optimize` task using the previous task as input.
810    pub fn optimize_with<F>(self, configure: F) -> Self
811    where
812        F: FnOnce(OptimizeTask) -> OptimizeTask,
813    {
814        let input = self.previous_input();
815        self.append_configured_task(OptimizeTask::new(input), configure)
816    }
817
818    /// Appends and configures a text `watermark` task using the previous task as input.
819    pub fn watermark_text_with<F>(self, text: impl Into<String>, configure: F) -> Self
820    where
821        F: FnOnce(WatermarkTask) -> WatermarkTask,
822    {
823        let input = self.previous_input();
824        self.append_configured_task(WatermarkTask::text(input, text), configure)
825    }
826
827    /// Appends and configures an image `watermark` task using the previous task as input.
828    pub fn watermark_image_with<F>(self, image_task_name: impl Into<String>, configure: F) -> Self
829    where
830        F: FnOnce(WatermarkTask) -> WatermarkTask,
831    {
832        let input = self.previous_input();
833        self.append_configured_task(WatermarkTask::image(input, image_task_name), configure)
834    }
835
836    /// Appends and configures a `capture-website` task.
837    pub fn capture_website_with<F>(
838        self,
839        url: impl Into<String>,
840        output_format: impl Into<String>,
841        configure: F,
842    ) -> Self
843    where
844        F: FnOnce(CaptureWebsiteTask) -> CaptureWebsiteTask,
845    {
846        self.append_configured_task(CaptureWebsiteTask::new(url, output_format), configure)
847    }
848
849    /// Appends and configures a `thumbnail` task using the previous task as input.
850    pub fn thumbnail_with<F>(self, output_format: impl Into<String>, configure: F) -> Self
851    where
852        F: FnOnce(ThumbnailTask) -> ThumbnailTask,
853    {
854        let input = self.previous_input();
855        self.append_configured_task(ThumbnailTask::new(input, output_format), configure)
856    }
857
858    /// Appends and configures a `metadata` task using the previous task as input.
859    pub fn metadata_with<F>(self, configure: F) -> Self
860    where
861        F: FnOnce(MetadataTask) -> MetadataTask,
862    {
863        let input = self.previous_input();
864        self.append_configured_task(MetadataTask::new(input), configure)
865    }
866
867    /// Appends and configures a `metadata/write` task using the previous task as input.
868    pub fn metadata_write_with<F>(self, configure: F) -> Self
869    where
870        F: FnOnce(MetadataWriteTask) -> MetadataWriteTask,
871    {
872        let input = self.previous_input();
873        self.append_configured_task(MetadataWriteTask::new(input), configure)
874    }
875
876    /// Appends and configures a `merge` task using the previous task as input.
877    pub fn merge_with<F>(self, output_format: impl Into<String>, configure: F) -> Self
878    where
879        F: FnOnce(MergeTask) -> MergeTask,
880    {
881        let input = self.previous_input();
882        self.append_configured_task(MergeTask::new(input, output_format), configure)
883    }
884
885    /// Appends and configures an `archive` task using the previous task as input.
886    pub fn archive_with<F>(self, output_format: impl Into<String>, configure: F) -> Self
887    where
888        F: FnOnce(ArchiveTask) -> ArchiveTask,
889    {
890        let input = self.previous_input();
891        self.append_configured_task(ArchiveTask::new(input, output_format), configure)
892    }
893
894    /// Appends and configures a `command` task using the previous task as input.
895    pub fn command_with<F>(
896        self,
897        engine: impl Into<String>,
898        command: impl Into<String>,
899        arguments: impl Into<String>,
900        configure: F,
901    ) -> Self
902    where
903        F: FnOnce(CommandTask) -> CommandTask,
904    {
905        let input = self.previous_input();
906        self.append_configured_task(
907            CommandTask::new(input, engine, command, arguments),
908            configure,
909        )
910    }
911
912    /// Appends and configures an `export/url` task using the previous task as input.
913    pub fn export_url_with<F>(self, configure: F) -> Self
914    where
915        F: FnOnce(ExportUrlTask) -> ExportUrlTask,
916    {
917        let input = self.previous_input();
918        self.append_configured_task(ExportUrlTask::new(input), configure)
919    }
920
921    /// Appends and configures an `export/s3` task using the previous task as input.
922    pub fn export_s3_with<F>(
923        self,
924        bucket: impl Into<String>,
925        region: impl Into<String>,
926        access_key_id: impl Into<String>,
927        secret_access_key: impl Into<String>,
928        configure: F,
929    ) -> Self
930    where
931        F: FnOnce(S3ExportTask) -> S3ExportTask,
932    {
933        let input = self.previous_input();
934        self.append_configured_task(
935            S3ExportTask::new(input, bucket, region, access_key_id, secret_access_key),
936            configure,
937        )
938    }
939
940    /// Appends and configures an `export/azure/blob` task using the previous task as input.
941    pub fn export_azure_blob_with<F>(
942        self,
943        storage_account: impl Into<String>,
944        container: impl Into<String>,
945        configure: F,
946    ) -> Self
947    where
948        F: FnOnce(AzureBlobExportTask) -> AzureBlobExportTask,
949    {
950        let input = self.previous_input();
951        self.append_configured_task(
952            AzureBlobExportTask::new(input, storage_account, container),
953            configure,
954        )
955    }
956
957    /// Appends and configures an `export/google-cloud-storage` task using the previous task as input.
958    pub fn export_google_cloud_storage_with<F>(
959        self,
960        project_id: impl Into<String>,
961        bucket: impl Into<String>,
962        client_email: impl Into<String>,
963        private_key: impl Into<String>,
964        configure: F,
965    ) -> Self
966    where
967        F: FnOnce(GoogleCloudStorageExportTask) -> GoogleCloudStorageExportTask,
968    {
969        let input = self.previous_input();
970        self.append_configured_task(
971            GoogleCloudStorageExportTask::new(input, project_id, bucket, client_email, private_key),
972            configure,
973        )
974    }
975
976    /// Appends and configures an `export/openstack` task using the previous task as input.
977    pub fn export_openstack_with<F>(
978        self,
979        auth_url: impl Into<String>,
980        username: impl Into<String>,
981        password: impl Into<String>,
982        region: impl Into<String>,
983        container: impl Into<String>,
984        configure: F,
985    ) -> Self
986    where
987        F: FnOnce(OpenStackExportTask) -> OpenStackExportTask,
988    {
989        let input = self.previous_input();
990        self.append_configured_task(
991            OpenStackExportTask::new(input, auth_url, username, password, region, container),
992            configure,
993        )
994    }
995
996    /// Appends and configures an `export/sftp` task using the previous task as input.
997    pub fn export_sftp_with<F>(
998        self,
999        host: impl Into<String>,
1000        username: impl Into<String>,
1001        configure: F,
1002    ) -> Self
1003    where
1004        F: FnOnce(SftpExportTask) -> SftpExportTask,
1005    {
1006        let input = self.previous_input();
1007        self.append_configured_task(SftpExportTask::new(input, host, username), configure)
1008    }
1009
1010    /// Appends and configures an `export/upload` task using the previous task as input.
1011    pub fn export_upload_with<F>(self, url: impl Into<String>, configure: F) -> Self
1012    where
1013        F: FnOnce(ExportUploadTask) -> ExportUploadTask,
1014    {
1015        let input = self.previous_input();
1016        self.append_configured_task(ExportUploadTask::new(input, url), configure)
1017    }
1018
1019    /// Finishes the builder and returns the job creation request.
1020    pub fn build(self) -> JobCreateRequest {
1021        self.request
1022    }
1023
1024    fn append_task(mut self, task: impl Into<TaskRequest>) -> Self {
1025        self.add_task(task);
1026        self
1027    }
1028
1029    fn append_configured_task<T, F>(self, task: T, configure: F) -> Self
1030    where
1031        T: Into<TaskRequest>,
1032        F: FnOnce(T) -> T,
1033    {
1034        self.append_task(configure(task))
1035    }
1036
1037    fn previous_input(&self) -> Input {
1038        Input::from(
1039            self.last_task
1040                .as_ref()
1041                .expect("job builder shorthand requires a previous task"),
1042        )
1043    }
1044}
1045
1046fn generated_task_name(operation: &str, existing: &BTreeMap<String, TaskRequest>) -> TaskName {
1047    let base = task_name_base(operation);
1048
1049    if !existing.contains_key(&base) {
1050        return TaskName::new(base);
1051    }
1052
1053    let mut counter = 2;
1054    loop {
1055        let candidate = format!("{base}-{counter}");
1056        if !existing.contains_key(&candidate) {
1057            return TaskName::new(candidate);
1058        }
1059        counter += 1;
1060    }
1061}
1062
1063fn task_name_base(operation: &str) -> String {
1064    let mut name = String::new();
1065    let mut previous_was_separator = false;
1066
1067    for byte in operation.bytes() {
1068        if byte.is_ascii_alphanumeric() {
1069            name.push(byte.to_ascii_lowercase() as char);
1070            previous_was_separator = false;
1071        } else if !previous_was_separator && !name.is_empty() {
1072            name.push('-');
1073            previous_was_separator = true;
1074        }
1075    }
1076
1077    while name.ends_with('-') {
1078        name.pop();
1079    }
1080
1081    if name.is_empty() {
1082        "task".to_string()
1083    } else {
1084        name
1085    }
1086}
1087
1088impl From<JobBuilder> for JobCreateRequest {
1089    fn from(builder: JobBuilder) -> Self {
1090        builder.build()
1091    }
1092}
1093
1094/// Builder for branched CloudConvert job graphs.
1095///
1096/// `JobGraphBuilder` is usually used through [`JobCreateRequest::graph`]. Each
1097/// task method appends one task and returns a [`TaskName`] handle that can be
1098/// passed to later graph methods.
1099///
1100/// ```
1101/// use cloudconvert_sdk::{FileExtension, JobCreateRequest};
1102///
1103/// let request = JobCreateRequest::graph(|job| {
1104///     let imported = job.import_url("https://example.test/input.docx");
1105///     let pdf = job.convert(&imported, FileExtension::Pdf);
1106///     let png = job.convert(&imported, FileExtension::Png);
1107///     job.export_url(vec![&pdf, &png]);
1108/// })
1109/// .build();
1110///
1111/// let payload = serde_json::to_value(request).unwrap();
1112/// assert_eq!(payload["tasks"]["export-url"]["input"], serde_json::json!(["convert", "convert-2"]));
1113/// ```
1114#[derive(Clone, Debug, Default)]
1115pub struct JobGraphBuilder {
1116    builder: JobBuilder,
1117}
1118
1119impl JobGraphBuilder {
1120    /// Creates an empty graph builder.
1121    ///
1122    /// Prefer [`JobCreateRequest::graph`] when the graph can be configured in a
1123    /// single closure.
1124    pub fn new() -> Self {
1125        Self::default()
1126    }
1127
1128    /// Sets an optional job tag.
1129    pub fn tag(&mut self, tag: impl Into<String>) -> &mut Self {
1130        self.builder.request.tag = Some(tag.into());
1131        self
1132    }
1133
1134    /// Sets the webhook URL CloudConvert should call for this job.
1135    pub fn webhook_url(&mut self, webhook_url: impl Into<String>) -> &mut Self {
1136        self.builder.request.webhook_url = Some(webhook_url.into());
1137        self
1138    }
1139
1140    /// Sets whether CloudConvert should redirect on synchronous job completion.
1141    pub fn redirect(&mut self, redirect: bool) -> &mut Self {
1142        self.builder.request.redirect = Some(redirect);
1143        self
1144    }
1145
1146    /// Adds a custom top-level job field.
1147    pub fn option(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
1148        self.builder.request.extra.insert(key.into(), value.into());
1149        self
1150    }
1151
1152    /// Adds a task with a generated task name and returns that name as a handle.
1153    pub fn add_task(&mut self, task: impl Into<TaskRequest>) -> TaskName {
1154        self.builder.add_task(task)
1155    }
1156
1157    /// Adds a task with an explicit task name and returns that name as a handle.
1158    pub fn add_named_task(
1159        &mut self,
1160        name: impl Into<String>,
1161        task: impl Into<TaskRequest>,
1162    ) -> TaskName {
1163        self.builder.add_named_task(name, task)
1164    }
1165
1166    /// Adds an `import/url` task.
1167    pub fn import_url(&mut self, url: impl Into<String>) -> TaskName {
1168        self.import_url_with(url, identity)
1169    }
1170
1171    /// Adds and configures an `import/url` task.
1172    pub fn import_url_with<F>(&mut self, url: impl Into<String>, configure: F) -> TaskName
1173    where
1174        F: FnOnce(ImportUrlTask) -> ImportUrlTask,
1175    {
1176        self.add_configured_task(ImportUrlTask::new(url), configure)
1177    }
1178
1179    /// Adds an `import/upload` task.
1180    pub fn import_upload(&mut self) -> TaskName {
1181        self.import_upload_with(identity)
1182    }
1183
1184    /// Adds and configures an `import/upload` task.
1185    pub fn import_upload_with<F>(&mut self, configure: F) -> TaskName
1186    where
1187        F: FnOnce(ImportUploadTask) -> ImportUploadTask,
1188    {
1189        self.add_configured_task(ImportUploadTask::default(), configure)
1190    }
1191
1192    /// Adds an `import/base64` task.
1193    pub fn import_base64(
1194        &mut self,
1195        file: impl Into<String>,
1196        filename: impl Into<String>,
1197    ) -> TaskName {
1198        self.add_task(Base64ImportTask::new(file, filename))
1199    }
1200
1201    /// Adds an `import/raw` task.
1202    pub fn import_raw(&mut self, file: impl Into<String>, filename: impl Into<String>) -> TaskName {
1203        self.add_task(RawImportTask::new(file, filename))
1204    }
1205
1206    /// Adds an `import/s3` task.
1207    pub fn import_s3(
1208        &mut self,
1209        bucket: impl Into<String>,
1210        region: impl Into<String>,
1211        access_key_id: impl Into<String>,
1212        secret_access_key: impl Into<String>,
1213    ) -> TaskName {
1214        self.import_s3_with(bucket, region, access_key_id, secret_access_key, identity)
1215    }
1216
1217    /// Adds and configures an `import/s3` task.
1218    pub fn import_s3_with<F>(
1219        &mut self,
1220        bucket: impl Into<String>,
1221        region: impl Into<String>,
1222        access_key_id: impl Into<String>,
1223        secret_access_key: impl Into<String>,
1224        configure: F,
1225    ) -> TaskName
1226    where
1227        F: FnOnce(S3ImportTask) -> S3ImportTask,
1228    {
1229        self.add_configured_task(
1230            S3ImportTask::new(bucket, region, access_key_id, secret_access_key),
1231            configure,
1232        )
1233    }
1234
1235    /// Adds an `import/azure/blob` task.
1236    pub fn import_azure_blob(
1237        &mut self,
1238        storage_account: impl Into<String>,
1239        container: impl Into<String>,
1240    ) -> TaskName {
1241        self.import_azure_blob_with(storage_account, container, identity)
1242    }
1243
1244    /// Adds and configures an `import/azure/blob` task.
1245    pub fn import_azure_blob_with<F>(
1246        &mut self,
1247        storage_account: impl Into<String>,
1248        container: impl Into<String>,
1249        configure: F,
1250    ) -> TaskName
1251    where
1252        F: FnOnce(AzureBlobImportTask) -> AzureBlobImportTask,
1253    {
1254        self.add_configured_task(
1255            AzureBlobImportTask::new(storage_account, container),
1256            configure,
1257        )
1258    }
1259
1260    /// Adds an `import/google-cloud-storage` task.
1261    pub fn import_google_cloud_storage(
1262        &mut self,
1263        project_id: impl Into<String>,
1264        bucket: impl Into<String>,
1265        client_email: impl Into<String>,
1266        private_key: impl Into<String>,
1267    ) -> TaskName {
1268        self.import_google_cloud_storage_with(
1269            project_id,
1270            bucket,
1271            client_email,
1272            private_key,
1273            identity,
1274        )
1275    }
1276
1277    /// Adds and configures an `import/google-cloud-storage` task.
1278    pub fn import_google_cloud_storage_with<F>(
1279        &mut self,
1280        project_id: impl Into<String>,
1281        bucket: impl Into<String>,
1282        client_email: impl Into<String>,
1283        private_key: impl Into<String>,
1284        configure: F,
1285    ) -> TaskName
1286    where
1287        F: FnOnce(GoogleCloudStorageImportTask) -> GoogleCloudStorageImportTask,
1288    {
1289        self.add_configured_task(
1290            GoogleCloudStorageImportTask::new(project_id, bucket, client_email, private_key),
1291            configure,
1292        )
1293    }
1294
1295    /// Adds an `import/openstack` task.
1296    pub fn import_openstack(
1297        &mut self,
1298        auth_url: impl Into<String>,
1299        username: impl Into<String>,
1300        password: impl Into<String>,
1301        region: impl Into<String>,
1302        container: impl Into<String>,
1303    ) -> TaskName {
1304        self.import_openstack_with(auth_url, username, password, region, container, identity)
1305    }
1306
1307    /// Adds and configures an `import/openstack` task.
1308    pub fn import_openstack_with<F>(
1309        &mut self,
1310        auth_url: impl Into<String>,
1311        username: impl Into<String>,
1312        password: impl Into<String>,
1313        region: impl Into<String>,
1314        container: impl Into<String>,
1315        configure: F,
1316    ) -> TaskName
1317    where
1318        F: FnOnce(OpenStackImportTask) -> OpenStackImportTask,
1319    {
1320        self.add_configured_task(
1321            OpenStackImportTask::new(auth_url, username, password, region, container),
1322            configure,
1323        )
1324    }
1325
1326    /// Adds an `import/sftp` task.
1327    pub fn import_sftp(
1328        &mut self,
1329        host: impl Into<String>,
1330        username: impl Into<String>,
1331    ) -> TaskName {
1332        self.import_sftp_with(host, username, identity)
1333    }
1334
1335    /// Adds and configures an `import/sftp` task.
1336    pub fn import_sftp_with<F>(
1337        &mut self,
1338        host: impl Into<String>,
1339        username: impl Into<String>,
1340        configure: F,
1341    ) -> TaskName
1342    where
1343        F: FnOnce(SftpImportTask) -> SftpImportTask,
1344    {
1345        self.add_configured_task(SftpImportTask::new(host, username), configure)
1346    }
1347
1348    /// Adds a `convert` task.
1349    pub fn convert(
1350        &mut self,
1351        input: impl Into<Input>,
1352        output_format: impl Into<String>,
1353    ) -> TaskName {
1354        self.convert_with(input, output_format, identity)
1355    }
1356
1357    /// Adds and configures a `convert` task.
1358    pub fn convert_with<F>(
1359        &mut self,
1360        input: impl Into<Input>,
1361        output_format: impl Into<String>,
1362        configure: F,
1363    ) -> TaskName
1364    where
1365        F: FnOnce(ConvertTask) -> ConvertTask,
1366    {
1367        self.add_configured_task(ConvertTask::new(input, output_format), configure)
1368    }
1369
1370    /// Adds an `optimize` task.
1371    pub fn optimize(&mut self, input: impl Into<Input>) -> TaskName {
1372        self.optimize_with(input, identity)
1373    }
1374
1375    /// Adds and configures an `optimize` task.
1376    pub fn optimize_with<F>(&mut self, input: impl Into<Input>, configure: F) -> TaskName
1377    where
1378        F: FnOnce(OptimizeTask) -> OptimizeTask,
1379    {
1380        self.add_configured_task(OptimizeTask::new(input), configure)
1381    }
1382
1383    /// Adds a text `watermark` task.
1384    pub fn watermark_text(&mut self, input: impl Into<Input>, text: impl Into<String>) -> TaskName {
1385        self.watermark_text_with(input, text, identity)
1386    }
1387
1388    /// Adds and configures a text `watermark` task.
1389    pub fn watermark_text_with<F>(
1390        &mut self,
1391        input: impl Into<Input>,
1392        text: impl Into<String>,
1393        configure: F,
1394    ) -> TaskName
1395    where
1396        F: FnOnce(WatermarkTask) -> WatermarkTask,
1397    {
1398        self.add_configured_task(WatermarkTask::text(input, text), configure)
1399    }
1400
1401    /// Adds an image `watermark` task.
1402    pub fn watermark_image(
1403        &mut self,
1404        input: impl Into<Input>,
1405        image_task_name: impl Into<String>,
1406    ) -> TaskName {
1407        self.watermark_image_with(input, image_task_name, identity)
1408    }
1409
1410    /// Adds and configures an image `watermark` task.
1411    pub fn watermark_image_with<F>(
1412        &mut self,
1413        input: impl Into<Input>,
1414        image_task_name: impl Into<String>,
1415        configure: F,
1416    ) -> TaskName
1417    where
1418        F: FnOnce(WatermarkTask) -> WatermarkTask,
1419    {
1420        self.add_configured_task(WatermarkTask::image(input, image_task_name), configure)
1421    }
1422
1423    /// Adds a `capture-website` task.
1424    pub fn capture_website(
1425        &mut self,
1426        url: impl Into<String>,
1427        output_format: impl Into<String>,
1428    ) -> TaskName {
1429        self.capture_website_with(url, output_format, identity)
1430    }
1431
1432    /// Adds and configures a `capture-website` task.
1433    pub fn capture_website_with<F>(
1434        &mut self,
1435        url: impl Into<String>,
1436        output_format: impl Into<String>,
1437        configure: F,
1438    ) -> TaskName
1439    where
1440        F: FnOnce(CaptureWebsiteTask) -> CaptureWebsiteTask,
1441    {
1442        self.add_configured_task(CaptureWebsiteTask::new(url, output_format), configure)
1443    }
1444
1445    /// Adds a `thumbnail` task.
1446    pub fn thumbnail(
1447        &mut self,
1448        input: impl Into<Input>,
1449        output_format: impl Into<String>,
1450    ) -> TaskName {
1451        self.thumbnail_with(input, output_format, identity)
1452    }
1453
1454    /// Adds and configures a `thumbnail` task.
1455    pub fn thumbnail_with<F>(
1456        &mut self,
1457        input: impl Into<Input>,
1458        output_format: impl Into<String>,
1459        configure: F,
1460    ) -> TaskName
1461    where
1462        F: FnOnce(ThumbnailTask) -> ThumbnailTask,
1463    {
1464        self.add_configured_task(ThumbnailTask::new(input, output_format), configure)
1465    }
1466
1467    /// Adds a `metadata` task.
1468    pub fn metadata(&mut self, input: impl Into<Input>) -> TaskName {
1469        self.metadata_with(input, identity)
1470    }
1471
1472    /// Adds and configures a `metadata` task.
1473    pub fn metadata_with<F>(&mut self, input: impl Into<Input>, configure: F) -> TaskName
1474    where
1475        F: FnOnce(MetadataTask) -> MetadataTask,
1476    {
1477        self.add_configured_task(MetadataTask::new(input), configure)
1478    }
1479
1480    /// Adds a `metadata/write` task.
1481    pub fn metadata_write(&mut self, input: impl Into<Input>) -> TaskName {
1482        self.metadata_write_with(input, identity)
1483    }
1484
1485    /// Adds and configures a `metadata/write` task.
1486    pub fn metadata_write_with<F>(&mut self, input: impl Into<Input>, configure: F) -> TaskName
1487    where
1488        F: FnOnce(MetadataWriteTask) -> MetadataWriteTask,
1489    {
1490        self.add_configured_task(MetadataWriteTask::new(input), configure)
1491    }
1492
1493    /// Adds a `merge` task.
1494    pub fn merge(&mut self, input: impl Into<Input>, output_format: impl Into<String>) -> TaskName {
1495        self.merge_with(input, output_format, identity)
1496    }
1497
1498    /// Adds and configures a `merge` task.
1499    pub fn merge_with<F>(
1500        &mut self,
1501        input: impl Into<Input>,
1502        output_format: impl Into<String>,
1503        configure: F,
1504    ) -> TaskName
1505    where
1506        F: FnOnce(MergeTask) -> MergeTask,
1507    {
1508        self.add_configured_task(MergeTask::new(input, output_format), configure)
1509    }
1510
1511    /// Adds an `archive` task.
1512    pub fn archive(
1513        &mut self,
1514        input: impl Into<Input>,
1515        output_format: impl Into<String>,
1516    ) -> TaskName {
1517        self.archive_with(input, output_format, identity)
1518    }
1519
1520    /// Adds and configures an `archive` task.
1521    pub fn archive_with<F>(
1522        &mut self,
1523        input: impl Into<Input>,
1524        output_format: impl Into<String>,
1525        configure: F,
1526    ) -> TaskName
1527    where
1528        F: FnOnce(ArchiveTask) -> ArchiveTask,
1529    {
1530        self.add_configured_task(ArchiveTask::new(input, output_format), configure)
1531    }
1532
1533    /// Adds a `command` task.
1534    pub fn command(
1535        &mut self,
1536        input: impl Into<Input>,
1537        engine: impl Into<String>,
1538        command: impl Into<String>,
1539        arguments: impl Into<String>,
1540    ) -> TaskName {
1541        self.command_with(input, engine, command, arguments, identity)
1542    }
1543
1544    /// Adds and configures a `command` task.
1545    pub fn command_with<F>(
1546        &mut self,
1547        input: impl Into<Input>,
1548        engine: impl Into<String>,
1549        command: impl Into<String>,
1550        arguments: impl Into<String>,
1551        configure: F,
1552    ) -> TaskName
1553    where
1554        F: FnOnce(CommandTask) -> CommandTask,
1555    {
1556        self.add_configured_task(
1557            CommandTask::new(input, engine, command, arguments),
1558            configure,
1559        )
1560    }
1561
1562    graph_pdf_task_methods!(pdf_a, pdf_a_with, PdfATask);
1563    graph_pdf_task_methods!(pdf_x, pdf_x_with, PdfXTask);
1564    graph_pdf_task_methods!(pdf_ocr, pdf_ocr_with, PdfOcrTask);
1565    graph_pdf_task_methods!(pdf_encrypt, pdf_encrypt_with, PdfEncryptTask);
1566    graph_pdf_task_methods!(pdf_decrypt, pdf_decrypt_with, PdfDecryptTask);
1567    graph_pdf_task_methods!(pdf_split_pages, pdf_split_pages_with, PdfSplitPagesTask);
1568    graph_pdf_task_methods!(
1569        pdf_extract_pages,
1570        pdf_extract_pages_with,
1571        PdfExtractPagesTask
1572    );
1573    graph_pdf_task_methods!(pdf_rotate_pages, pdf_rotate_pages_with, PdfRotatePagesTask);
1574
1575    /// Adds an `export/url` task.
1576    pub fn export_url(&mut self, input: impl Into<Input>) -> TaskName {
1577        self.export_url_with(input, identity)
1578    }
1579
1580    /// Adds and configures an `export/url` task.
1581    pub fn export_url_with<F>(&mut self, input: impl Into<Input>, configure: F) -> TaskName
1582    where
1583        F: FnOnce(ExportUrlTask) -> ExportUrlTask,
1584    {
1585        self.add_configured_task(ExportUrlTask::new(input), configure)
1586    }
1587
1588    /// Adds an `export/s3` task.
1589    pub fn export_s3(
1590        &mut self,
1591        input: impl Into<Input>,
1592        bucket: impl Into<String>,
1593        region: impl Into<String>,
1594        access_key_id: impl Into<String>,
1595        secret_access_key: impl Into<String>,
1596    ) -> TaskName {
1597        self.export_s3_with(
1598            input,
1599            bucket,
1600            region,
1601            access_key_id,
1602            secret_access_key,
1603            identity,
1604        )
1605    }
1606
1607    /// Adds and configures an `export/s3` task.
1608    pub fn export_s3_with<F>(
1609        &mut self,
1610        input: impl Into<Input>,
1611        bucket: impl Into<String>,
1612        region: impl Into<String>,
1613        access_key_id: impl Into<String>,
1614        secret_access_key: impl Into<String>,
1615        configure: F,
1616    ) -> TaskName
1617    where
1618        F: FnOnce(S3ExportTask) -> S3ExportTask,
1619    {
1620        self.add_configured_task(
1621            S3ExportTask::new(input, bucket, region, access_key_id, secret_access_key),
1622            configure,
1623        )
1624    }
1625
1626    /// Adds an `export/azure/blob` task.
1627    pub fn export_azure_blob(
1628        &mut self,
1629        input: impl Into<Input>,
1630        storage_account: impl Into<String>,
1631        container: impl Into<String>,
1632    ) -> TaskName {
1633        self.export_azure_blob_with(input, storage_account, container, identity)
1634    }
1635
1636    /// Adds and configures an `export/azure/blob` task.
1637    pub fn export_azure_blob_with<F>(
1638        &mut self,
1639        input: impl Into<Input>,
1640        storage_account: impl Into<String>,
1641        container: impl Into<String>,
1642        configure: F,
1643    ) -> TaskName
1644    where
1645        F: FnOnce(AzureBlobExportTask) -> AzureBlobExportTask,
1646    {
1647        self.add_configured_task(
1648            AzureBlobExportTask::new(input, storage_account, container),
1649            configure,
1650        )
1651    }
1652
1653    /// Adds an `export/google-cloud-storage` task.
1654    pub fn export_google_cloud_storage(
1655        &mut self,
1656        input: impl Into<Input>,
1657        project_id: impl Into<String>,
1658        bucket: impl Into<String>,
1659        client_email: impl Into<String>,
1660        private_key: impl Into<String>,
1661    ) -> TaskName {
1662        self.export_google_cloud_storage_with(
1663            input,
1664            project_id,
1665            bucket,
1666            client_email,
1667            private_key,
1668            identity,
1669        )
1670    }
1671
1672    /// Adds and configures an `export/google-cloud-storage` task.
1673    pub fn export_google_cloud_storage_with<F>(
1674        &mut self,
1675        input: impl Into<Input>,
1676        project_id: impl Into<String>,
1677        bucket: impl Into<String>,
1678        client_email: impl Into<String>,
1679        private_key: impl Into<String>,
1680        configure: F,
1681    ) -> TaskName
1682    where
1683        F: FnOnce(GoogleCloudStorageExportTask) -> GoogleCloudStorageExportTask,
1684    {
1685        self.add_configured_task(
1686            GoogleCloudStorageExportTask::new(input, project_id, bucket, client_email, private_key),
1687            configure,
1688        )
1689    }
1690
1691    /// Adds an `export/openstack` task.
1692    pub fn export_openstack(
1693        &mut self,
1694        input: impl Into<Input>,
1695        auth_url: impl Into<String>,
1696        username: impl Into<String>,
1697        password: impl Into<String>,
1698        region: impl Into<String>,
1699        container: impl Into<String>,
1700    ) -> TaskName {
1701        self.export_openstack_with(
1702            input, auth_url, username, password, region, container, identity,
1703        )
1704    }
1705
1706    /// Adds and configures an `export/openstack` task.
1707    #[allow(clippy::too_many_arguments)]
1708    pub fn export_openstack_with<F>(
1709        &mut self,
1710        input: impl Into<Input>,
1711        auth_url: impl Into<String>,
1712        username: impl Into<String>,
1713        password: impl Into<String>,
1714        region: impl Into<String>,
1715        container: impl Into<String>,
1716        configure: F,
1717    ) -> TaskName
1718    where
1719        F: FnOnce(OpenStackExportTask) -> OpenStackExportTask,
1720    {
1721        self.add_configured_task(
1722            OpenStackExportTask::new(input, auth_url, username, password, region, container),
1723            configure,
1724        )
1725    }
1726
1727    /// Adds an `export/sftp` task.
1728    pub fn export_sftp(
1729        &mut self,
1730        input: impl Into<Input>,
1731        host: impl Into<String>,
1732        username: impl Into<String>,
1733    ) -> TaskName {
1734        self.export_sftp_with(input, host, username, identity)
1735    }
1736
1737    /// Adds and configures an `export/sftp` task.
1738    pub fn export_sftp_with<F>(
1739        &mut self,
1740        input: impl Into<Input>,
1741        host: impl Into<String>,
1742        username: impl Into<String>,
1743        configure: F,
1744    ) -> TaskName
1745    where
1746        F: FnOnce(SftpExportTask) -> SftpExportTask,
1747    {
1748        self.add_configured_task(SftpExportTask::new(input, host, username), configure)
1749    }
1750
1751    /// Adds an `export/upload` task.
1752    pub fn export_upload(&mut self, input: impl Into<Input>, url: impl Into<String>) -> TaskName {
1753        self.export_upload_with(input, url, identity)
1754    }
1755
1756    /// Adds and configures an `export/upload` task.
1757    pub fn export_upload_with<F>(
1758        &mut self,
1759        input: impl Into<Input>,
1760        url: impl Into<String>,
1761        configure: F,
1762    ) -> TaskName
1763    where
1764        F: FnOnce(ExportUploadTask) -> ExportUploadTask,
1765    {
1766        self.add_configured_task(ExportUploadTask::new(input, url), configure)
1767    }
1768
1769    /// Finishes the graph builder and returns a regular [`JobBuilder`].
1770    pub fn into_builder(self) -> JobBuilder {
1771        self.builder
1772    }
1773
1774    /// Finishes the graph builder and returns the job creation request.
1775    pub fn build(self) -> JobCreateRequest {
1776        self.into_builder().build()
1777    }
1778
1779    fn add_configured_task<T, F>(&mut self, task: T, configure: F) -> TaskName
1780    where
1781        T: Into<TaskRequest>,
1782        F: FnOnce(T) -> T,
1783    {
1784        self.add_task(configure(task))
1785    }
1786}
1787
1788impl From<JobGraphBuilder> for JobBuilder {
1789    fn from(builder: JobGraphBuilder) -> Self {
1790        builder.into_builder()
1791    }
1792}
1793
1794impl From<JobGraphBuilder> for JobCreateRequest {
1795    fn from(builder: JobGraphBuilder) -> Self {
1796        builder.build()
1797    }
1798}
1799
1800#[derive(Clone, Debug, Default, Serialize)]
1801pub struct JobListQuery {
1802    #[serde(rename = "filter[status]", skip_serializing_if = "Option::is_none")]
1803    filter_status: Option<JobStatus>,
1804    #[serde(rename = "filter[tag]", skip_serializing_if = "Option::is_none")]
1805    filter_tag: Option<String>,
1806    #[serde(skip_serializing_if = "Option::is_none")]
1807    include: Option<String>,
1808    #[serde(skip_serializing_if = "Option::is_none")]
1809    per_page: Option<u32>,
1810    #[serde(skip_serializing_if = "Option::is_none")]
1811    page: Option<u32>,
1812}
1813
1814impl JobListQuery {
1815    pub fn status(mut self, status: JobStatus) -> Self {
1816        self.filter_status = Some(status);
1817        self
1818    }
1819
1820    pub fn tag(mut self, tag: impl Into<String>) -> Self {
1821        self.filter_tag = Some(tag.into());
1822        self
1823    }
1824
1825    pub fn include(mut self, include: impl Into<String>) -> Self {
1826        self.include = Some(include.into());
1827        self
1828    }
1829
1830    pub fn per_page(mut self, per_page: u32) -> Self {
1831        self.per_page = Some(per_page);
1832        self
1833    }
1834
1835    pub fn page(mut self, page: u32) -> Self {
1836        self.page = Some(page);
1837        self
1838    }
1839}
1840
1841#[derive(Clone, Debug, Default, Serialize)]
1842pub struct JobGetQuery {
1843    #[serde(skip_serializing_if = "Option::is_none")]
1844    include: Option<String>,
1845    #[serde(skip_serializing_if = "Option::is_none")]
1846    redirect: Option<bool>,
1847}
1848
1849impl JobGetQuery {
1850    pub fn include(mut self, include: impl Into<String>) -> Self {
1851        self.include = Some(include.into());
1852        self
1853    }
1854
1855    pub fn redirect(mut self, redirect: bool) -> Self {
1856        self.redirect = Some(redirect);
1857        self
1858    }
1859}
1860
1861#[derive(Clone, Debug, Default, Serialize)]
1862pub struct TaskListQuery {
1863    #[serde(rename = "filter[job_id]", skip_serializing_if = "Option::is_none")]
1864    filter_job_id: Option<String>,
1865    #[serde(rename = "filter[status]", skip_serializing_if = "Option::is_none")]
1866    filter_status: Option<TaskStatus>,
1867    #[serde(rename = "filter[operation]", skip_serializing_if = "Option::is_none")]
1868    filter_operation: Option<String>,
1869    #[serde(skip_serializing_if = "Option::is_none")]
1870    include: Option<String>,
1871    #[serde(skip_serializing_if = "Option::is_none")]
1872    per_page: Option<u32>,
1873    #[serde(skip_serializing_if = "Option::is_none")]
1874    page: Option<u32>,
1875}
1876
1877impl TaskListQuery {
1878    pub fn job_id(mut self, job_id: impl Into<String>) -> Self {
1879        self.filter_job_id = Some(job_id.into());
1880        self
1881    }
1882
1883    pub fn status(mut self, status: TaskStatus) -> Self {
1884        self.filter_status = Some(status);
1885        self
1886    }
1887
1888    pub fn operation(mut self, operation: impl Into<String>) -> Self {
1889        self.filter_operation = Some(operation.into());
1890        self
1891    }
1892
1893    pub fn include(mut self, include: impl Into<String>) -> Self {
1894        self.include = Some(include.into());
1895        self
1896    }
1897
1898    pub fn per_page(mut self, per_page: u32) -> Self {
1899        self.per_page = Some(per_page);
1900        self
1901    }
1902
1903    pub fn page(mut self, page: u32) -> Self {
1904        self.page = Some(page);
1905        self
1906    }
1907}
1908
1909#[derive(Clone, Debug, Default, Serialize)]
1910pub struct TaskGetQuery {
1911    #[serde(skip_serializing_if = "Option::is_none")]
1912    include: Option<String>,
1913}
1914
1915impl TaskGetQuery {
1916    pub fn include(mut self, include: impl Into<String>) -> Self {
1917        self.include = Some(include.into());
1918        self
1919    }
1920}
1921
1922#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1923#[serde(rename_all = "lowercase")]
1924#[non_exhaustive]
1925pub enum JobStatus {
1926    Waiting,
1927    Processing,
1928    Finished,
1929    Error,
1930    #[serde(other)]
1931    Unknown,
1932}
1933
1934impl JobStatus {
1935    pub fn is_waiting(&self) -> bool {
1936        matches!(self, Self::Waiting)
1937    }
1938
1939    pub fn is_processing(&self) -> bool {
1940        matches!(self, Self::Processing)
1941    }
1942
1943    pub fn is_finished(&self) -> bool {
1944        matches!(self, Self::Finished)
1945    }
1946
1947    pub fn is_error(&self) -> bool {
1948        matches!(self, Self::Error)
1949    }
1950
1951    pub fn is_terminal(&self) -> bool {
1952        matches!(self, Self::Finished | Self::Error)
1953    }
1954}
1955
1956#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1957#[serde(rename_all = "lowercase")]
1958#[non_exhaustive]
1959pub enum TaskStatus {
1960    Waiting,
1961    Queued,
1962    Processing,
1963    Finished,
1964    Error,
1965    #[serde(other)]
1966    Unknown,
1967}
1968
1969impl TaskStatus {
1970    pub fn is_waiting(&self) -> bool {
1971        matches!(self, Self::Waiting)
1972    }
1973
1974    pub fn is_queued(&self) -> bool {
1975        matches!(self, Self::Queued)
1976    }
1977
1978    pub fn is_processing(&self) -> bool {
1979        matches!(self, Self::Processing)
1980    }
1981
1982    pub fn is_finished(&self) -> bool {
1983        matches!(self, Self::Finished)
1984    }
1985
1986    pub fn is_error(&self) -> bool {
1987        matches!(self, Self::Error)
1988    }
1989
1990    pub fn is_terminal(&self) -> bool {
1991        matches!(self, Self::Finished | Self::Error)
1992    }
1993}
1994
1995#[derive(Clone, Deserialize, Serialize)]
1996#[non_exhaustive]
1997pub struct Job {
1998    pub id: String,
1999    #[serde(default)]
2000    pub tag: Option<String>,
2001    pub status: JobStatus,
2002    #[serde(default)]
2003    pub created_at: Option<String>,
2004    #[serde(default)]
2005    pub started_at: Option<String>,
2006    #[serde(default)]
2007    pub ended_at: Option<String>,
2008    #[serde(default)]
2009    pub tasks: Vec<JobTask>,
2010    #[serde(default)]
2011    pub links: BTreeMap<String, Value>,
2012    #[serde(flatten)]
2013    pub extra: BTreeMap<String, Value>,
2014}
2015
2016impl Job {
2017    pub fn is_finished(&self) -> bool {
2018        self.status.is_finished()
2019    }
2020
2021    pub fn is_error(&self) -> bool {
2022        self.status.is_error()
2023    }
2024
2025    pub fn is_terminal(&self) -> bool {
2026        self.status.is_terminal()
2027    }
2028
2029    pub fn export_tasks(&self) -> impl Iterator<Item = &JobTask> {
2030        self.tasks.iter().filter(|task| task.is_export_url())
2031    }
2032
2033    pub fn finished_export_tasks(&self) -> impl Iterator<Item = &JobTask> {
2034        self.export_tasks().filter(|task| task.is_finished())
2035    }
2036
2037    pub fn export_urls(&self) -> Vec<&FileResult> {
2038        self.finished_export_tasks()
2039            .flat_map(JobTask::files)
2040            .collect()
2041    }
2042}
2043
2044impl fmt::Debug for Job {
2045    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2046        f.debug_struct("Job")
2047            .field("id", &self.id)
2048            .field("tag", &self.tag)
2049            .field("status", &self.status)
2050            .field("created_at", &self.created_at)
2051            .field("started_at", &self.started_at)
2052            .field("ended_at", &self.ended_at)
2053            .field("tasks", &self.tasks)
2054            .field("links", &self.links)
2055            .field("extra", &RedactedValueMap(&self.extra))
2056            .finish()
2057    }
2058}
2059
2060#[derive(Clone, Deserialize, Serialize)]
2061#[non_exhaustive]
2062pub struct JobTask {
2063    #[serde(default)]
2064    pub id: Option<String>,
2065    pub name: String,
2066    pub operation: String,
2067    pub status: TaskStatus,
2068    #[serde(default)]
2069    pub message: Option<String>,
2070    #[serde(default)]
2071    pub code: Option<String>,
2072    #[serde(default)]
2073    pub credits: Option<f64>,
2074    #[serde(default)]
2075    pub created_at: Option<String>,
2076    #[serde(default)]
2077    pub started_at: Option<String>,
2078    #[serde(default)]
2079    pub ended_at: Option<String>,
2080    #[serde(default)]
2081    pub result: Option<TaskResult>,
2082    #[serde(default)]
2083    pub payload: Option<Value>,
2084    #[serde(flatten)]
2085    pub extra: BTreeMap<String, Value>,
2086}
2087
2088impl JobTask {
2089    pub fn is_finished(&self) -> bool {
2090        self.status.is_finished()
2091    }
2092
2093    pub fn is_error(&self) -> bool {
2094        self.status.is_error()
2095    }
2096
2097    pub fn is_terminal(&self) -> bool {
2098        self.status.is_terminal()
2099    }
2100
2101    pub fn is_export_url(&self) -> bool {
2102        self.operation == "export/url"
2103    }
2104
2105    pub fn files(&self) -> impl Iterator<Item = &FileResult> {
2106        self.result
2107            .as_ref()
2108            .into_iter()
2109            .flat_map(|result| result.files.iter())
2110    }
2111}
2112
2113impl fmt::Debug for JobTask {
2114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2115        f.debug_struct("JobTask")
2116            .field("id", &self.id)
2117            .field("name", &self.name)
2118            .field("operation", &self.operation)
2119            .field("status", &self.status)
2120            .field("message", &self.message)
2121            .field("code", &self.code)
2122            .field("credits", &self.credits)
2123            .field("created_at", &self.created_at)
2124            .field("started_at", &self.started_at)
2125            .field("ended_at", &self.ended_at)
2126            .field("result", &self.result)
2127            .field("payload", &redacted_option(&self.payload))
2128            .field("extra", &RedactedValueMap(&self.extra))
2129            .finish()
2130    }
2131}
2132
2133#[derive(Clone, Deserialize, Serialize)]
2134#[non_exhaustive]
2135pub struct Task {
2136    pub id: String,
2137    #[serde(default)]
2138    pub job_id: Option<String>,
2139    pub operation: String,
2140    pub status: TaskStatus,
2141    #[serde(default)]
2142    pub message: Option<String>,
2143    #[serde(default)]
2144    pub code: Option<String>,
2145    #[serde(default)]
2146    pub credits: Option<f64>,
2147    #[serde(default)]
2148    pub created_at: Option<String>,
2149    #[serde(default)]
2150    pub started_at: Option<String>,
2151    #[serde(default)]
2152    pub ended_at: Option<String>,
2153    #[serde(default)]
2154    pub depends_on_tasks: BTreeMap<String, String>,
2155    #[serde(default)]
2156    pub result: Option<TaskResult>,
2157    #[serde(default)]
2158    pub payload: Option<Value>,
2159    #[serde(flatten)]
2160    pub extra: BTreeMap<String, Value>,
2161}
2162
2163impl Task {
2164    pub fn is_finished(&self) -> bool {
2165        self.status.is_finished()
2166    }
2167
2168    pub fn is_error(&self) -> bool {
2169        self.status.is_error()
2170    }
2171
2172    pub fn is_terminal(&self) -> bool {
2173        self.status.is_terminal()
2174    }
2175
2176    pub fn is_import_upload(&self) -> bool {
2177        self.operation == "import/upload"
2178    }
2179
2180    pub fn upload_form(&self) -> Option<&UploadForm> {
2181        self.result
2182            .as_ref()
2183            .and_then(|result| result.form.as_ref())
2184            .filter(|_| self.is_import_upload())
2185    }
2186
2187    pub fn is_upload_ready(&self) -> bool {
2188        self.upload_form().is_some()
2189    }
2190
2191    pub fn files(&self) -> impl Iterator<Item = &FileResult> {
2192        self.result
2193            .as_ref()
2194            .into_iter()
2195            .flat_map(|result| result.files.iter())
2196    }
2197}
2198
2199impl fmt::Debug for Task {
2200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2201        f.debug_struct("Task")
2202            .field("id", &self.id)
2203            .field("job_id", &self.job_id)
2204            .field("operation", &self.operation)
2205            .field("status", &self.status)
2206            .field("message", &self.message)
2207            .field("code", &self.code)
2208            .field("credits", &self.credits)
2209            .field("created_at", &self.created_at)
2210            .field("started_at", &self.started_at)
2211            .field("ended_at", &self.ended_at)
2212            .field("depends_on_tasks", &self.depends_on_tasks)
2213            .field("result", &self.result)
2214            .field("payload", &redacted_option(&self.payload))
2215            .field("extra", &RedactedValueMap(&self.extra))
2216            .finish()
2217    }
2218}
2219
2220#[derive(Clone, Default, Deserialize, Serialize)]
2221#[non_exhaustive]
2222pub struct TaskResult {
2223    #[serde(default)]
2224    pub files: Vec<FileResult>,
2225    #[serde(default)]
2226    pub form: Option<UploadForm>,
2227    #[serde(flatten)]
2228    pub extra: BTreeMap<String, Value>,
2229}
2230
2231impl fmt::Debug for TaskResult {
2232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2233        f.debug_struct("TaskResult")
2234            .field("files", &self.files)
2235            .field("form", &self.form)
2236            .field("extra", &RedactedValueMap(&self.extra))
2237            .finish()
2238    }
2239}
2240
2241#[derive(Clone, Debug, Deserialize, Serialize)]
2242#[non_exhaustive]
2243pub struct FileResult {
2244    #[serde(default)]
2245    pub dir: Option<String>,
2246    pub filename: String,
2247    #[serde(default)]
2248    pub url: Option<String>,
2249    #[serde(default)]
2250    pub size: Option<u64>,
2251    #[serde(flatten)]
2252    pub extra: BTreeMap<String, Value>,
2253}
2254
2255#[derive(Clone, Deserialize, Serialize)]
2256#[non_exhaustive]
2257pub struct UploadForm {
2258    pub url: String,
2259    #[serde(default)]
2260    pub parameters: BTreeMap<String, Value>,
2261}
2262
2263impl fmt::Debug for UploadForm {
2264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2265        f.debug_struct("UploadForm")
2266            .field("url", &self.url)
2267            .field("parameters", &RedactedValueMap(&self.parameters))
2268            .finish()
2269    }
2270}
2271
2272#[derive(Debug, Deserialize)]
2273pub(crate) struct DataEnvelope<T> {
2274    pub data: T,
2275    #[serde(default)]
2276    pub links: PaginationLinks,
2277    #[serde(default)]
2278    pub meta: PaginationMeta,
2279}