Skip to main content

cloudconvert_sdk/
tasks.rs

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