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}