Skip to main content

cloudconvert_sdk/
tasks.rs

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