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