Skip to main content

cloudiful_docling_convert/document/
input_kind.rs

1use std::path::Path;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::{PdfConvertError, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InputKind {
11    Pdf,
12    Doc,
13    Docx,
14    Ppt,
15    Pptx,
16    Html,
17    Asciidoc,
18    Markdown,
19    Csv,
20    Xlsx,
21    Xls,
22    Odt,
23    Ods,
24    Odp,
25    Epub,
26    Email,
27    Image,
28    XmlUspto,
29    XmlJats,
30    XmlXbrl,
31    XmlDoclang,
32    MetsGbs,
33    JsonDocling,
34    Dclx,
35    Audio,
36    Video,
37    Vtt,
38    Boxnote,
39    Latex,
40    Text,
41}
42
43#[derive(Debug, Clone, Copy)]
44struct InputKindSpec {
45    kind: InputKind,
46    extensions: &'static [&'static str],
47    media_types: &'static [&'static str],
48    default_extension: &'static str,
49    default_media_type: &'static str,
50    from_formats_value: &'static str,
51    parse_aliases: &'static [&'static str],
52    reading_label: &'static str,
53    auto_detect: bool,
54    generic_convert_options: bool,
55    supports_vlm: bool,
56}
57
58const INPUT_KIND_SPECS: &[InputKindSpec] = &[
59    InputKindSpec {
60        kind: InputKind::Pdf,
61        extensions: &["pdf"],
62        media_types: &["application/pdf"],
63        default_extension: "pdf",
64        default_media_type: "application/pdf",
65        from_formats_value: "pdf",
66        parse_aliases: &["pdf"],
67        reading_label: "Reading PDF...",
68        auto_detect: true,
69        generic_convert_options: false,
70        supports_vlm: true,
71    },
72    InputKindSpec {
73        kind: InputKind::Doc,
74        extensions: &["doc"],
75        media_types: &["application/msword"],
76        default_extension: "doc",
77        default_media_type: "application/msword",
78        from_formats_value: "doc",
79        parse_aliases: &["doc"],
80        reading_label: "Reading DOC...",
81        auto_detect: true,
82        generic_convert_options: true,
83        supports_vlm: true,
84    },
85    InputKindSpec {
86        kind: InputKind::Docx,
87        extensions: &["docx"],
88        media_types: &["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
89        default_extension: "docx",
90        default_media_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
91        from_formats_value: "docx",
92        parse_aliases: &["docx"],
93        reading_label: "Reading DOCX...",
94        auto_detect: true,
95        generic_convert_options: true,
96        supports_vlm: true,
97    },
98    InputKindSpec {
99        kind: InputKind::Pptx,
100        extensions: &["pptx"],
101        media_types: &["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
102        default_extension: "pptx",
103        default_media_type: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
104        from_formats_value: "pptx",
105        parse_aliases: &["pptx"],
106        reading_label: "Reading PPTX...",
107        auto_detect: true,
108        generic_convert_options: true,
109        supports_vlm: false,
110    },
111    InputKindSpec {
112        kind: InputKind::Ppt,
113        extensions: &["ppt"],
114        media_types: &["application/vnd.ms-powerpoint"],
115        default_extension: "ppt",
116        default_media_type: "application/vnd.ms-powerpoint",
117        from_formats_value: "ppt",
118        parse_aliases: &["ppt"],
119        reading_label: "Reading PPT...",
120        auto_detect: true,
121        generic_convert_options: true,
122        supports_vlm: false,
123    },
124    InputKindSpec {
125        kind: InputKind::Html,
126        extensions: &["html", "htm", "xhtml"],
127        media_types: &["text/html", "application/xhtml+xml"],
128        default_extension: "html",
129        default_media_type: "text/html",
130        from_formats_value: "html",
131        parse_aliases: &["html"],
132        reading_label: "Reading HTML...",
133        auto_detect: true,
134        generic_convert_options: true,
135        supports_vlm: false,
136    },
137    InputKindSpec {
138        kind: InputKind::Asciidoc,
139        extensions: &["adoc", "asciidoc", "asc"],
140        media_types: &["text/asciidoc", "text/x-asciidoc"],
141        default_extension: "adoc",
142        default_media_type: "text/asciidoc",
143        from_formats_value: "asciidoc",
144        parse_aliases: &["asciidoc", "adoc"],
145        reading_label: "Reading AsciiDoc...",
146        auto_detect: true,
147        generic_convert_options: true,
148        supports_vlm: false,
149    },
150    InputKindSpec {
151        kind: InputKind::Markdown,
152        extensions: &["md", "markdown"],
153        media_types: &["text/markdown", "text/x-markdown"],
154        default_extension: "md",
155        default_media_type: "text/markdown",
156        from_formats_value: "md",
157        parse_aliases: &["markdown", "md"],
158        reading_label: "Reading Markdown...",
159        auto_detect: true,
160        generic_convert_options: true,
161        supports_vlm: true,
162    },
163    InputKindSpec {
164        kind: InputKind::Csv,
165        extensions: &["csv"],
166        media_types: &["text/csv", "application/csv"],
167        default_extension: "csv",
168        default_media_type: "text/csv",
169        from_formats_value: "csv",
170        parse_aliases: &["csv"],
171        reading_label: "Reading CSV...",
172        auto_detect: true,
173        generic_convert_options: true,
174        supports_vlm: false,
175    },
176    InputKindSpec {
177        kind: InputKind::Xlsx,
178        extensions: &["xlsx"],
179        media_types: &["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
180        default_extension: "xlsx",
181        default_media_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
182        from_formats_value: "xlsx",
183        parse_aliases: &["xlsx"],
184        reading_label: "Reading XLSX...",
185        auto_detect: true,
186        generic_convert_options: true,
187        supports_vlm: false,
188    },
189    InputKindSpec {
190        kind: InputKind::Xls,
191        extensions: &["xls"],
192        media_types: &["application/vnd.ms-excel"],
193        default_extension: "xls",
194        default_media_type: "application/vnd.ms-excel",
195        from_formats_value: "xls",
196        parse_aliases: &["xls"],
197        reading_label: "Reading XLS...",
198        auto_detect: true,
199        generic_convert_options: true,
200        supports_vlm: false,
201    },
202    InputKindSpec {
203        kind: InputKind::Odt,
204        extensions: &["odt"],
205        media_types: &["application/vnd.oasis.opendocument.text"],
206        default_extension: "odt",
207        default_media_type: "application/vnd.oasis.opendocument.text",
208        from_formats_value: "odt",
209        parse_aliases: &["odt"],
210        reading_label: "Reading ODT...",
211        auto_detect: true,
212        generic_convert_options: true,
213        supports_vlm: false,
214    },
215    InputKindSpec {
216        kind: InputKind::Ods,
217        extensions: &["ods"],
218        media_types: &["application/vnd.oasis.opendocument.spreadsheet"],
219        default_extension: "ods",
220        default_media_type: "application/vnd.oasis.opendocument.spreadsheet",
221        from_formats_value: "ods",
222        parse_aliases: &["ods"],
223        reading_label: "Reading ODS...",
224        auto_detect: true,
225        generic_convert_options: true,
226        supports_vlm: false,
227    },
228    InputKindSpec {
229        kind: InputKind::Odp,
230        extensions: &["odp"],
231        media_types: &["application/vnd.oasis.opendocument.presentation"],
232        default_extension: "odp",
233        default_media_type: "application/vnd.oasis.opendocument.presentation",
234        from_formats_value: "odp",
235        parse_aliases: &["odp"],
236        reading_label: "Reading ODP...",
237        auto_detect: true,
238        generic_convert_options: true,
239        supports_vlm: false,
240    },
241    InputKindSpec {
242        kind: InputKind::Epub,
243        extensions: &["epub"],
244        media_types: &["application/epub+zip"],
245        default_extension: "epub",
246        default_media_type: "application/epub+zip",
247        from_formats_value: "epub",
248        parse_aliases: &["epub"],
249        reading_label: "Reading EPUB...",
250        auto_detect: true,
251        generic_convert_options: true,
252        supports_vlm: false,
253    },
254    InputKindSpec {
255        kind: InputKind::Email,
256        extensions: &["eml", "msg"],
257        media_types: &["message/rfc822", "application/vnd.ms-outlook"],
258        default_extension: "eml",
259        default_media_type: "message/rfc822",
260        from_formats_value: "email",
261        parse_aliases: &["email", "eml", "msg"],
262        reading_label: "Reading email...",
263        auto_detect: true,
264        generic_convert_options: true,
265        supports_vlm: false,
266    },
267    InputKindSpec {
268        kind: InputKind::Image,
269        extensions: &[
270            "png", "jpg", "jpeg", "gif", "bmp", "tif", "tiff", "webp", "svg",
271        ],
272        media_types: &[
273            "image/png",
274            "image/jpeg",
275            "image/gif",
276            "image/bmp",
277            "image/tiff",
278            "image/webp",
279            "image/svg+xml",
280        ],
281        default_extension: "png",
282        default_media_type: "image/png",
283        from_formats_value: "image",
284        parse_aliases: &["image"],
285        reading_label: "Reading image...",
286        auto_detect: true,
287        generic_convert_options: true,
288        supports_vlm: true,
289    },
290    InputKindSpec {
291        kind: InputKind::XmlUspto,
292        extensions: &["xml"],
293        media_types: &["application/xml", "text/xml"],
294        default_extension: "xml",
295        default_media_type: "application/xml",
296        from_formats_value: "xml_uspto",
297        parse_aliases: &["xml_uspto"],
298        reading_label: "Reading XML USPTO...",
299        auto_detect: false,
300        generic_convert_options: true,
301        supports_vlm: false,
302    },
303    InputKindSpec {
304        kind: InputKind::XmlJats,
305        extensions: &["xml"],
306        media_types: &["application/xml", "text/xml"],
307        default_extension: "xml",
308        default_media_type: "application/xml",
309        from_formats_value: "xml_jats",
310        parse_aliases: &["xml_jats"],
311        reading_label: "Reading XML JATS...",
312        auto_detect: false,
313        generic_convert_options: true,
314        supports_vlm: false,
315    },
316    InputKindSpec {
317        kind: InputKind::XmlXbrl,
318        extensions: &["xml"],
319        media_types: &["application/xml", "text/xml"],
320        default_extension: "xml",
321        default_media_type: "application/xml",
322        from_formats_value: "xml_xbrl",
323        parse_aliases: &["xml_xbrl"],
324        reading_label: "Reading XML XBRL...",
325        auto_detect: false,
326        generic_convert_options: true,
327        supports_vlm: false,
328    },
329    InputKindSpec {
330        kind: InputKind::XmlDoclang,
331        extensions: &["xml"],
332        media_types: &["application/xml", "text/xml"],
333        default_extension: "xml",
334        default_media_type: "application/xml",
335        from_formats_value: "xml_doclang",
336        parse_aliases: &["xml_doclang"],
337        reading_label: "Reading XML Docling...",
338        auto_detect: false,
339        generic_convert_options: true,
340        supports_vlm: false,
341    },
342    InputKindSpec {
343        kind: InputKind::MetsGbs,
344        extensions: &["xml"],
345        media_types: &["application/xml", "text/xml"],
346        default_extension: "xml",
347        default_media_type: "application/xml",
348        from_formats_value: "mets_gbs",
349        parse_aliases: &["mets_gbs"],
350        reading_label: "Reading METS GBS...",
351        auto_detect: false,
352        generic_convert_options: true,
353        supports_vlm: false,
354    },
355    InputKindSpec {
356        kind: InputKind::JsonDocling,
357        extensions: &["json"],
358        media_types: &["application/json", "text/json"],
359        default_extension: "json",
360        default_media_type: "application/json",
361        from_formats_value: "json_docling",
362        parse_aliases: &["json_docling"],
363        reading_label: "Reading JSON Docling...",
364        auto_detect: false,
365        generic_convert_options: true,
366        supports_vlm: false,
367    },
368    InputKindSpec {
369        kind: InputKind::Latex,
370        extensions: &["tex"],
371        media_types: &[
372            "application/x-tex",
373            "application/x-latex",
374            "text/x-tex",
375            "text/x-latex",
376        ],
377        default_extension: "tex",
378        default_media_type: "application/x-tex",
379        from_formats_value: "latex",
380        parse_aliases: &["latex", "tex"],
381        reading_label: "Reading LaTeX...",
382        auto_detect: true,
383        generic_convert_options: true,
384        supports_vlm: false,
385    },
386    InputKindSpec {
387        kind: InputKind::Dclx,
388        extensions: &["dclx"],
389        media_types: &["application/zip", "application/vnd.docling.dclx"],
390        default_extension: "dclx",
391        default_media_type: "application/vnd.docling.dclx",
392        from_formats_value: "dclx",
393        parse_aliases: &["dclx"],
394        reading_label: "Reading DCLX...",
395        auto_detect: true,
396        generic_convert_options: true,
397        supports_vlm: false,
398    },
399    InputKindSpec {
400        kind: InputKind::Audio,
401        extensions: &["wav", "mp3", "m4a", "flac", "ogg", "aac"],
402        media_types: &[
403            "audio/wav",
404            "audio/x-wav",
405            "audio/mpeg",
406            "audio/mp4",
407            "audio/flac",
408            "audio/ogg",
409            "audio/aac",
410        ],
411        default_extension: "wav",
412        default_media_type: "audio/wav",
413        from_formats_value: "audio",
414        parse_aliases: &["audio"],
415        reading_label: "Reading audio...",
416        auto_detect: true,
417        generic_convert_options: true,
418        supports_vlm: false,
419    },
420    InputKindSpec {
421        kind: InputKind::Video,
422        extensions: &["mp4", "mov", "avi", "mkv", "webm"],
423        media_types: &[
424            "video/mp4",
425            "video/quicktime",
426            "video/x-msvideo",
427            "video/x-matroska",
428            "video/webm",
429        ],
430        default_extension: "mp4",
431        default_media_type: "video/mp4",
432        from_formats_value: "video",
433        parse_aliases: &["video"],
434        reading_label: "Reading video...",
435        auto_detect: true,
436        generic_convert_options: true,
437        supports_vlm: false,
438    },
439    InputKindSpec {
440        kind: InputKind::Vtt,
441        extensions: &["vtt"],
442        media_types: &["text/vtt"],
443        default_extension: "vtt",
444        default_media_type: "text/vtt",
445        from_formats_value: "vtt",
446        parse_aliases: &["vtt"],
447        reading_label: "Reading VTT...",
448        auto_detect: true,
449        generic_convert_options: true,
450        supports_vlm: false,
451    },
452    InputKindSpec {
453        kind: InputKind::Boxnote,
454        extensions: &["boxnote"],
455        media_types: &["application/boxnote", "application/vnd.box.note"],
456        default_extension: "boxnote",
457        default_media_type: "application/boxnote",
458        from_formats_value: "boxnote",
459        parse_aliases: &["boxnote"],
460        reading_label: "Reading Box Note...",
461        auto_detect: true,
462        generic_convert_options: true,
463        supports_vlm: false,
464    },
465    InputKindSpec {
466        kind: InputKind::Text,
467        extensions: &["txt"],
468        media_types: &["text/plain"],
469        default_extension: "txt",
470        default_media_type: "text/plain",
471        from_formats_value: "text",
472        parse_aliases: &["text", "txt"],
473        reading_label: "Reading text file...",
474        auto_detect: true,
475        generic_convert_options: false,
476        supports_vlm: false,
477    },
478];
479
480impl InputKind {
481    pub fn from_path(path: &Path) -> Option<Self> {
482        let file_name = path.file_name().and_then(|name| name.to_str())?;
483        Self::from_filename_and_media_type(file_name, None)
484    }
485
486    pub fn from_filename_and_media_type(filename: &str, media_type: Option<&str>) -> Option<Self> {
487        let ext = normalized_extension(filename);
488        let media_type = normalized_media_type(media_type);
489
490        if Self::requires_explicit_override(filename, media_type.as_deref()) {
491            return None;
492        }
493
494        detect_auto_kind(ext.as_deref(), media_type.as_deref())
495    }
496
497    pub fn requires_explicit_override(filename: &str, media_type: Option<&str>) -> bool {
498        let ext = normalized_extension(filename);
499        let media_type = normalized_media_type(media_type);
500
501        matches!(ext.as_deref(), Some("xml") | Some("json"))
502            || matches!(
503                media_type.as_deref(),
504                Some("application/xml")
505                    | Some("text/xml")
506                    | Some("application/json")
507                    | Some("text/json")
508            )
509    }
510
511    pub fn media_type(self) -> &'static str {
512        self.spec().default_media_type
513    }
514
515    pub fn canonical_media_type(self, filename: &str, media_type: Option<&str>) -> &'static str {
516        let ext = normalized_extension(filename);
517        let media_type = normalized_media_type(media_type);
518
519        match self {
520            Self::Html => match (ext.as_deref(), media_type.as_deref()) {
521                (Some("xhtml"), _) | (_, Some("application/xhtml+xml")) => "application/xhtml+xml",
522                _ => self.media_type(),
523            },
524            Self::Email => match (ext.as_deref(), media_type.as_deref()) {
525                (Some("msg"), _) | (_, Some("application/vnd.ms-outlook")) => {
526                    "application/vnd.ms-outlook"
527                }
528                _ => self.media_type(),
529            },
530            Self::Image => image_media_type(ext.as_deref(), media_type.as_deref()),
531            Self::Audio => audio_media_type(ext.as_deref(), media_type.as_deref()),
532            Self::Video => video_media_type(ext.as_deref(), media_type.as_deref()),
533            _ => self.media_type(),
534        }
535    }
536
537    pub fn default_extension(self) -> &'static str {
538        self.spec().default_extension
539    }
540
541    pub fn default_extension_for_media_type(self, media_type: Option<&str>) -> &'static str {
542        let media_type = normalized_media_type(media_type);
543
544        match self {
545            Self::Html => match media_type.as_deref() {
546                Some("application/xhtml+xml") => "xhtml",
547                _ => self.default_extension(),
548            },
549            Self::Email => match media_type.as_deref() {
550                Some("application/vnd.ms-outlook") => "msg",
551                _ => self.default_extension(),
552            },
553            Self::Image => image_extension(media_type.as_deref()),
554            _ => self.default_extension(),
555        }
556    }
557
558    pub fn reading_label(self) -> &'static str {
559        self.spec().reading_label
560    }
561
562    pub fn from_formats_value(self) -> &'static str {
563        self.spec().from_formats_value
564    }
565
566    pub fn uses_generic_convert_options(self) -> bool {
567        self.spec().generic_convert_options
568    }
569
570    pub fn supports_vlm(self) -> bool {
571        self.spec().supports_vlm
572    }
573
574    fn spec(self) -> &'static InputKindSpec {
575        INPUT_KIND_SPECS
576            .iter()
577            .find(|spec| spec.kind == self)
578            .expect("missing InputKindSpec")
579    }
580}
581
582impl std::fmt::Display for InputKind {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        f.write_str(self.from_formats_value())
585    }
586}
587
588impl FromStr for InputKind {
589    type Err = PdfConvertError;
590
591    fn from_str(value: &str) -> Result<Self> {
592        let normalized = value.trim().to_ascii_lowercase();
593
594        INPUT_KIND_SPECS
595            .iter()
596            .find(|spec| spec.parse_aliases.contains(&normalized.as_str()))
597            .map(|spec| spec.kind)
598            .ok_or_else(|| {
599                PdfConvertError::validation_error(
600                    "input_format",
601                    format!("unsupported input format: {}", normalized),
602                )
603            })
604    }
605}
606
607fn detect_auto_kind(extension: Option<&str>, media_type: Option<&str>) -> Option<InputKind> {
608    INPUT_KIND_SPECS
609        .iter()
610        .filter(|spec| spec.auto_detect)
611        .find(|spec| {
612            extension.is_some_and(|ext| spec.extensions.contains(&ext))
613                || media_type.is_some_and(|mime| spec.media_types.contains(&mime))
614        })
615        .map(|spec| spec.kind)
616}
617
618fn image_media_type(extension: Option<&str>, media_type: Option<&str>) -> &'static str {
619    match (extension, media_type) {
620        (Some("jpg" | "jpeg"), _) | (_, Some("image/jpeg")) => "image/jpeg",
621        (Some("gif"), _) | (_, Some("image/gif")) => "image/gif",
622        (Some("bmp"), _) | (_, Some("image/bmp")) => "image/bmp",
623        (Some("tif" | "tiff"), _) | (_, Some("image/tiff")) => "image/tiff",
624        (Some("webp"), _) | (_, Some("image/webp")) => "image/webp",
625        (Some("svg"), _) | (_, Some("image/svg+xml")) => "image/svg+xml",
626        _ => "image/png",
627    }
628}
629
630fn image_extension(media_type: Option<&str>) -> &'static str {
631    match media_type {
632        Some("image/jpeg") => "jpg",
633        Some("image/gif") => "gif",
634        Some("image/bmp") => "bmp",
635        Some("image/tiff") => "tiff",
636        Some("image/webp") => "webp",
637        Some("image/svg+xml") => "svg",
638        _ => "png",
639    }
640}
641
642fn audio_media_type(extension: Option<&str>, media_type: Option<&str>) -> &'static str {
643    match (extension, media_type) {
644        (Some("mp3"), _) | (_, Some("audio/mpeg")) => "audio/mpeg",
645        (Some("m4a"), _) | (_, Some("audio/mp4")) => "audio/mp4",
646        (Some("flac"), _) | (_, Some("audio/flac")) => "audio/flac",
647        (Some("ogg"), _) | (_, Some("audio/ogg")) => "audio/ogg",
648        (Some("aac"), _) | (_, Some("audio/aac")) => "audio/aac",
649        _ => "audio/wav",
650    }
651}
652
653fn video_media_type(extension: Option<&str>, media_type: Option<&str>) -> &'static str {
654    match (extension, media_type) {
655        (Some("mov"), _) | (_, Some("video/quicktime")) => "video/quicktime",
656        (Some("avi"), _) | (_, Some("video/x-msvideo")) => "video/x-msvideo",
657        (Some("mkv"), _) | (_, Some("video/x-matroska")) => "video/x-matroska",
658        (Some("webm"), _) | (_, Some("video/webm")) => "video/webm",
659        _ => "video/mp4",
660    }
661}
662
663fn normalized_extension(filename: &str) -> Option<String> {
664    Path::new(filename)
665        .extension()
666        .and_then(|ext| ext.to_str())
667        .map(|ext| ext.to_ascii_lowercase())
668}
669
670fn normalized_media_type(media_type: Option<&str>) -> Option<String> {
671    media_type.map(|value| {
672        value
673            .split(';')
674            .next()
675            .unwrap_or(value)
676            .trim()
677            .to_ascii_lowercase()
678    })
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn metadata_methods_match_specs() {
687        for spec in INPUT_KIND_SPECS {
688            assert_eq!(spec.kind.default_extension(), spec.default_extension);
689            assert_eq!(spec.kind.media_type(), spec.default_media_type);
690            assert_eq!(spec.kind.from_formats_value(), spec.from_formats_value);
691            assert_eq!(spec.kind.reading_label(), spec.reading_label);
692            assert_eq!(
693                spec.kind.uses_generic_convert_options(),
694                spec.generic_convert_options
695            );
696            assert_eq!(spec.kind.supports_vlm(), spec.supports_vlm);
697        }
698    }
699
700    #[test]
701    fn detects_first_wave_and_latex_input_kinds_from_extension() {
702        for spec in INPUT_KIND_SPECS.iter().filter(|spec| spec.auto_detect) {
703            for extension in spec.extensions {
704                let filename = format!("sample.{}", extension);
705                assert_eq!(
706                    InputKind::from_path(Path::new(&filename)),
707                    Some(spec.kind),
708                    "failed to detect extension '{}'",
709                    extension
710                );
711            }
712        }
713    }
714
715    #[test]
716    fn generic_xml_and_json_do_not_auto_map() {
717        assert_eq!(InputKind::from_path(Path::new("a.xml")), None);
718        assert_eq!(InputKind::from_path(Path::new("a.json")), None);
719        assert_eq!(
720            InputKind::from_filename_and_media_type("downloaded", Some("application/xml")),
721            None
722        );
723        assert_eq!(
724            InputKind::from_filename_and_media_type("downloaded", Some("application/json")),
725            None
726        );
727    }
728
729    #[test]
730    fn detects_supported_input_kinds_from_mime_type() {
731        for spec in INPUT_KIND_SPECS.iter().filter(|spec| spec.auto_detect) {
732            for media_type in spec.media_types {
733                assert_eq!(
734                    InputKind::from_filename_and_media_type("upload", Some(media_type)),
735                    Some(spec.kind),
736                    "failed to detect media type '{}'",
737                    media_type
738                );
739            }
740        }
741    }
742
743    #[test]
744    fn reports_override_only_sources() {
745        assert!(InputKind::requires_explicit_override("paper.xml", None));
746        assert!(InputKind::requires_explicit_override(
747            "paper",
748            Some("application/xml")
749        ));
750        assert!(InputKind::requires_explicit_override("paper.json", None));
751        assert!(InputKind::requires_explicit_override(
752            "paper",
753            Some("application/json")
754        ));
755        assert!(!InputKind::requires_explicit_override("paper.tex", None));
756    }
757
758    #[test]
759    fn derives_second_wave_defaults() {
760        assert_eq!(InputKind::XmlJats.default_extension(), "xml");
761        assert_eq!(InputKind::JsonDocling.default_extension(), "json");
762        assert_eq!(InputKind::Latex.default_extension(), "tex");
763        assert_eq!(InputKind::XmlUspto.from_formats_value(), "xml_uspto");
764        assert_eq!(InputKind::JsonDocling.from_formats_value(), "json_docling");
765        assert_eq!(InputKind::Latex.from_formats_value(), "latex");
766    }
767
768    #[test]
769    fn parses_input_format_strings() {
770        for spec in INPUT_KIND_SPECS {
771            for alias in spec.parse_aliases {
772                assert_eq!(
773                    alias.parse::<InputKind>().unwrap(),
774                    spec.kind,
775                    "failed to parse input alias '{alias}'"
776                );
777            }
778        }
779        assert!("xml".parse::<InputKind>().is_err());
780    }
781
782    #[test]
783    fn preserves_specific_media_types() {
784        assert_eq!(
785            InputKind::Image.canonical_media_type("cover.jpg", None),
786            "image/jpeg"
787        );
788        assert_eq!(
789            InputKind::Email.canonical_media_type("message.msg", None),
790            "application/vnd.ms-outlook"
791        );
792        assert_eq!(
793            InputKind::Html.canonical_media_type("page.xhtml", None),
794            "application/xhtml+xml"
795        );
796        assert_eq!(
797            InputKind::Audio.canonical_media_type("recording.mp3", None),
798            "audio/mpeg"
799        );
800        assert_eq!(
801            InputKind::Audio.canonical_media_type("recording.m4a", None),
802            "audio/mp4"
803        );
804        assert_eq!(
805            InputKind::Video.canonical_media_type("clip.mov", None),
806            "video/quicktime"
807        );
808        assert_eq!(
809            InputKind::Video.canonical_media_type("clip.mkv", None),
810            "video/x-matroska"
811        );
812    }
813}