Skip to main content

cloudconvert_sdk/
jobs.rs

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