Skip to main content

cloudconvert_sdk/
operations.rs

1use std::collections::BTreeMap;
2
3use std::{error::Error as StdError, fmt};
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::Value;
7
8use crate::file_extension::normalize_file_extension;
9use crate::tasks::TaskRequest;
10
11#[derive(Clone, Debug, Default, Serialize)]
12pub struct OperationListQuery {
13    #[serde(rename = "filter[operation]", skip_serializing_if = "Option::is_none")]
14    operation: Option<String>,
15    #[serde(
16        rename = "filter[input_format]",
17        skip_serializing_if = "Option::is_none"
18    )]
19    input_format: Option<String>,
20    #[serde(
21        rename = "filter[output_format]",
22        skip_serializing_if = "Option::is_none"
23    )]
24    output_format: Option<String>,
25    #[serde(rename = "filter[engine]", skip_serializing_if = "Option::is_none")]
26    engine: Option<String>,
27    #[serde(
28        rename = "filter[engine_version]",
29        skip_serializing_if = "Option::is_none"
30    )]
31    engine_version: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    include: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    alternatives: Option<bool>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    page: Option<u32>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    per_page: Option<u32>,
40}
41
42impl OperationListQuery {
43    pub fn operation(mut self, operation: impl Into<String>) -> Self {
44        self.operation = Some(operation.into());
45        self
46    }
47
48    pub fn input_format(mut self, input_format: impl Into<String>) -> Self {
49        self.input_format = Some(normalize_file_extension(input_format));
50        self
51    }
52
53    pub fn output_format(mut self, output_format: impl Into<String>) -> Self {
54        self.output_format = Some(normalize_file_extension(output_format));
55        self
56    }
57
58    pub fn engine(mut self, engine: impl Into<String>) -> Self {
59        self.engine = Some(engine.into());
60        self
61    }
62
63    pub fn engine_version(mut self, engine_version: impl Into<String>) -> Self {
64        self.engine_version = Some(engine_version.into());
65        self
66    }
67
68    pub fn include(mut self, include: impl Into<String>) -> Self {
69        self.include = Some(include.into());
70        self
71    }
72
73    pub fn include_options(self) -> Self {
74        self.include("options")
75    }
76
77    pub fn include_engine_versions(self) -> Self {
78        self.include("engine_versions")
79    }
80
81    pub fn include_options_and_engine_versions(self) -> Self {
82        self.include("options,engine_versions")
83    }
84
85    pub fn alternatives(mut self, alternatives: bool) -> Self {
86        self.alternatives = Some(alternatives);
87        self
88    }
89
90    pub fn page(mut self, page: u32) -> Self {
91        self.page = Some(page);
92        self
93    }
94
95    pub fn per_page(mut self, per_page: u32) -> Self {
96        self.per_page = Some(per_page);
97        self
98    }
99}
100
101#[derive(Clone, Debug, Deserialize, Serialize)]
102#[non_exhaustive]
103pub struct Operation {
104    pub operation: String,
105    #[serde(default)]
106    pub input_format: Option<String>,
107    #[serde(default)]
108    pub output_format: Option<String>,
109    #[serde(default)]
110    pub engine: Option<String>,
111    #[serde(default)]
112    pub engine_version: Option<String>,
113    #[serde(default)]
114    pub credits: Option<u64>,
115    #[serde(default, deserialize_with = "deserialize_options")]
116    pub options: BTreeMap<String, OperationOption>,
117    #[serde(default, deserialize_with = "deserialize_engine_versions")]
118    pub engine_versions: Vec<OperationEngineVersion>,
119    #[serde(default)]
120    pub alternatives: Vec<Operation>,
121    #[serde(default)]
122    pub deprecated: Option<bool>,
123    #[serde(default)]
124    pub experimental: Option<bool>,
125    #[serde(default)]
126    pub meta: Option<BTreeMap<String, Value>>,
127    #[serde(flatten)]
128    pub extra: BTreeMap<String, Value>,
129}
130
131impl Operation {
132    pub fn option(&self, name: &str) -> Option<&OperationOption> {
133        self.options.get(name)
134    }
135
136    pub fn options(&self) -> impl Iterator<Item = (&str, &OperationOption)> {
137        self.options
138            .iter()
139            .map(|(name, option)| (name.as_str(), option))
140    }
141
142    pub fn engine_version_values(&self) -> impl Iterator<Item = &str> {
143        self.engine_versions
144            .iter()
145            .map(|version| version.version.as_str())
146    }
147
148    pub fn default_engine_version(&self) -> Option<&OperationEngineVersion> {
149        self.engine_versions
150            .iter()
151            .find(|version| version.default.unwrap_or(false))
152    }
153
154    pub fn latest_engine_version(&self) -> Option<&OperationEngineVersion> {
155        self.engine_versions
156            .iter()
157            .find(|version| version.latest.unwrap_or(false))
158    }
159
160    pub fn validate_task(&self, task: &TaskRequest) -> OperationValidationResult {
161        self.validate_task_with_mode(task, OperationValidationMode::Lenient)
162    }
163
164    pub fn validate_task_strict(&self, task: &TaskRequest) -> OperationValidationResult {
165        self.validate_task_with_mode(task, OperationValidationMode::Strict)
166    }
167
168    pub fn validate_task_with_mode(
169        &self,
170        task: &TaskRequest,
171        mode: OperationValidationMode,
172    ) -> OperationValidationResult {
173        if task.operation() != self.operation {
174            return Err(OperationValidationError::operation_mismatch(
175                &self.operation,
176                task.operation(),
177            ));
178        }
179
180        for (name, option) in &self.options {
181            let value = task.payload().get(name);
182            if option.required.unwrap_or(false) && value.is_none_or(Value::is_null) {
183                return Err(OperationValidationError::missing_required(
184                    &self.operation,
185                    name,
186                ));
187            }
188
189            let Some(value) = value else {
190                continue;
191            };
192            option.validate_value_for_operation(&self.operation, name, value)?;
193        }
194
195        if matches!(mode, OperationValidationMode::Strict) && !self.options.is_empty() {
196            for name in task.payload().keys() {
197                if !self.options.contains_key(name) && !is_common_task_field(name) {
198                    return Err(OperationValidationError::unknown_option(
199                        &self.operation,
200                        name,
201                    ));
202                }
203            }
204        }
205
206        Ok(())
207    }
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize)]
211#[non_exhaustive]
212pub struct OperationOption {
213    #[serde(default)]
214    pub name: Option<String>,
215    #[serde(default, rename = "type")]
216    pub kind: Option<OperationOptionKind>,
217    #[serde(default)]
218    pub label: Option<String>,
219    #[serde(default)]
220    pub description: Option<String>,
221    #[serde(default)]
222    pub required: Option<bool>,
223    #[serde(default)]
224    pub default: Option<Value>,
225    #[serde(default, alias = "values")]
226    pub possible_values: Vec<Value>,
227    #[serde(flatten)]
228    pub extra: BTreeMap<String, Value>,
229}
230
231impl OperationOption {
232    pub fn name(&self) -> Option<&str> {
233        self.name.as_deref()
234    }
235
236    pub fn kind(&self) -> Option<&OperationOptionKind> {
237        self.kind.as_ref()
238    }
239
240    pub fn is_required(&self) -> bool {
241        self.required.unwrap_or(false)
242    }
243
244    pub fn possible_values(&self) -> &[Value] {
245        &self.possible_values
246    }
247
248    pub fn validate_value(&self, value: &Value) -> OperationValidationResult {
249        self.validate_value_for_operation("", self.name.as_deref().unwrap_or("option"), value)
250    }
251
252    fn validate_value_for_operation(
253        &self,
254        operation: &str,
255        name: &str,
256        value: &Value,
257    ) -> OperationValidationResult {
258        if let Some(kind) = &self.kind
259            && !kind.matches_value(value)
260        {
261            return Err(OperationValidationError::invalid_type(
262                operation,
263                name,
264                kind.as_str(),
265                value_type(value),
266            ));
267        }
268
269        if !self.possible_values.is_empty()
270            && !self.possible_values.iter().any(|allowed| allowed == value)
271        {
272            return Err(OperationValidationError::invalid_value(
273                operation,
274                name,
275                "one of the documented possible_values",
276                value,
277            ));
278        }
279
280        Ok(())
281    }
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
285#[non_exhaustive]
286pub enum OperationOptionKind {
287    String,
288    Boolean,
289    Integer,
290    Float,
291    Enum,
292    Dictionary,
293    Array,
294    Other(String),
295}
296
297impl OperationOptionKind {
298    pub fn as_str(&self) -> &str {
299        match self {
300            Self::String => "string",
301            Self::Boolean => "boolean",
302            Self::Integer => "integer",
303            Self::Float => "float",
304            Self::Enum => "enum",
305            Self::Dictionary => "dictionary",
306            Self::Array => "array",
307            Self::Other(value) => value.as_str(),
308        }
309    }
310
311    fn matches_value(&self, value: &Value) -> bool {
312        match self {
313            Self::String => value.is_string(),
314            Self::Boolean => value.is_boolean(),
315            Self::Integer => value
316                .as_i64()
317                .or_else(|| value.as_u64().and_then(|value| i64::try_from(value).ok()))
318                .is_some(),
319            Self::Float => value.is_number(),
320            Self::Enum => value.is_string(),
321            Self::Dictionary => value.is_object(),
322            Self::Array => value.is_array(),
323            Self::Other(_) => true,
324        }
325    }
326}
327
328impl Serialize for OperationOptionKind {
329    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
330    where
331        S: Serializer,
332    {
333        serializer.serialize_str(self.as_str())
334    }
335}
336
337impl<'de> Deserialize<'de> for OperationOptionKind {
338    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
339    where
340        D: Deserializer<'de>,
341    {
342        let value = String::deserialize(deserializer)?;
343        Ok(match value.as_str() {
344            "string" => Self::String,
345            "boolean" => Self::Boolean,
346            "integer" => Self::Integer,
347            "float" => Self::Float,
348            "enum" => Self::Enum,
349            "dictionary" => Self::Dictionary,
350            "array" => Self::Array,
351            _ => Self::Other(value),
352        })
353    }
354}
355
356#[derive(Clone, Debug, Deserialize, Serialize)]
357#[non_exhaustive]
358pub struct OperationEngineVersion {
359    pub version: String,
360    #[serde(default)]
361    pub default: Option<bool>,
362    #[serde(default)]
363    pub latest: Option<bool>,
364    #[serde(default)]
365    pub deprecated: Option<bool>,
366    #[serde(default)]
367    pub experimental: Option<bool>,
368    #[serde(flatten)]
369    pub extra: BTreeMap<String, Value>,
370}
371
372#[derive(Clone, Copy, Debug, Eq, PartialEq)]
373#[non_exhaustive]
374pub enum OperationValidationMode {
375    Lenient,
376    Strict,
377}
378
379pub type OperationValidationResult = std::result::Result<(), OperationValidationError>;
380
381#[derive(Clone, Debug, Eq, PartialEq)]
382pub struct OperationValidationError {
383    pub kind: OperationValidationErrorKind,
384    pub operation: String,
385    pub option: Option<String>,
386    pub expected: Option<String>,
387    pub actual: Option<String>,
388}
389
390impl OperationValidationError {
391    fn operation_mismatch(expected: &str, actual: &str) -> Self {
392        Self {
393            kind: OperationValidationErrorKind::OperationMismatch,
394            operation: expected.to_string(),
395            option: None,
396            expected: Some(expected.to_string()),
397            actual: Some(actual.to_string()),
398        }
399    }
400
401    fn missing_required(operation: &str, option: &str) -> Self {
402        Self {
403            kind: OperationValidationErrorKind::MissingRequiredOption,
404            operation: operation.to_string(),
405            option: Some(option.to_string()),
406            expected: Some("present value".to_string()),
407            actual: Some("missing".to_string()),
408        }
409    }
410
411    fn invalid_type(operation: &str, option: &str, expected: &str, actual: &str) -> Self {
412        Self {
413            kind: OperationValidationErrorKind::InvalidOptionType,
414            operation: operation.to_string(),
415            option: Some(option.to_string()),
416            expected: Some(expected.to_string()),
417            actual: Some(actual.to_string()),
418        }
419    }
420
421    fn invalid_value(operation: &str, option: &str, expected: &str, actual: &Value) -> Self {
422        Self {
423            kind: OperationValidationErrorKind::InvalidOptionValue,
424            operation: operation.to_string(),
425            option: Some(option.to_string()),
426            expected: Some(expected.to_string()),
427            actual: Some(actual.to_string()),
428        }
429    }
430
431    fn unknown_option(operation: &str, option: &str) -> Self {
432        Self {
433            kind: OperationValidationErrorKind::UnknownOption,
434            operation: operation.to_string(),
435            option: Some(option.to_string()),
436            expected: Some("documented operation option".to_string()),
437            actual: Some("unknown option".to_string()),
438        }
439    }
440}
441
442impl fmt::Display for OperationValidationError {
443    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
444        match &self.option {
445            Some(option) => write!(
446                formatter,
447                "operation {} option {} failed validation: expected {}, got {}",
448                self.operation,
449                option,
450                self.expected.as_deref().unwrap_or("valid value"),
451                self.actual.as_deref().unwrap_or("invalid value")
452            ),
453            None => write!(
454                formatter,
455                "operation {} failed validation: expected {}, got {}",
456                self.operation,
457                self.expected.as_deref().unwrap_or("valid value"),
458                self.actual.as_deref().unwrap_or("invalid value")
459            ),
460        }
461    }
462}
463
464impl StdError for OperationValidationError {}
465
466#[derive(Clone, Debug, Eq, PartialEq)]
467#[non_exhaustive]
468pub enum OperationValidationErrorKind {
469    OperationMismatch,
470    MissingRequiredOption,
471    InvalidOptionType,
472    InvalidOptionValue,
473    UnknownOption,
474}
475
476#[derive(Deserialize)]
477#[serde(untagged)]
478enum OperationOptionsWire {
479    Map(BTreeMap<String, OperationOption>),
480    List(Vec<OperationOption>),
481}
482
483fn deserialize_options<'de, D>(
484    deserializer: D,
485) -> std::result::Result<BTreeMap<String, OperationOption>, D::Error>
486where
487    D: Deserializer<'de>,
488{
489    let wire = Option::<OperationOptionsWire>::deserialize(deserializer)?;
490    let mut options = BTreeMap::new();
491
492    match wire {
493        Some(OperationOptionsWire::Map(map)) => {
494            for (name, mut option) in map {
495                if option.name.is_none() {
496                    option.name = Some(name.clone());
497                }
498                options.insert(name, option);
499            }
500        }
501        Some(OperationOptionsWire::List(list)) => {
502            for option in list {
503                if let Some(name) = option.name.clone() {
504                    options.insert(name, option);
505                }
506            }
507        }
508        None => {}
509    }
510
511    Ok(options)
512}
513
514#[derive(Deserialize)]
515#[serde(untagged)]
516enum OperationEngineVersionWire {
517    String(String),
518    Object(OperationEngineVersion),
519}
520
521fn deserialize_engine_versions<'de, D>(
522    deserializer: D,
523) -> std::result::Result<Vec<OperationEngineVersion>, D::Error>
524where
525    D: Deserializer<'de>,
526{
527    let wire = Option::<Vec<OperationEngineVersionWire>>::deserialize(deserializer)?;
528    Ok(wire
529        .unwrap_or_default()
530        .into_iter()
531        .map(|version| match version {
532            OperationEngineVersionWire::String(version) => OperationEngineVersion {
533                version,
534                default: None,
535                latest: None,
536                deprecated: None,
537                experimental: None,
538                extra: BTreeMap::new(),
539            },
540            OperationEngineVersionWire::Object(version) => version,
541        })
542        .collect())
543}
544
545fn is_common_task_field(name: &str) -> bool {
546    matches!(
547        name,
548        "input"
549            | "ignore_error"
550            | "input_format"
551            | "output_format"
552            | "engine"
553            | "engine_version"
554            | "filename"
555            | "timeout"
556    )
557}
558
559fn value_type(value: &Value) -> &'static str {
560    match value {
561        Value::Null => "null",
562        Value::Bool(_) => "boolean",
563        Value::Number(number) if number.is_i64() || number.is_u64() => "integer",
564        Value::Number(_) => "float",
565        Value::String(_) => "string",
566        Value::Array(_) => "array",
567        Value::Object(_) => "dictionary",
568    }
569}