Skip to main content

kael_document/
file_type.rs

1//! File type matching for document formats.
2
3use std::path::Path;
4
5/// A supported document file type.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct FileType {
8    /// The human-readable file type name.
9    pub name: &'static str,
10    /// The supported filename extensions, without leading dots.
11    pub extensions: &'static [&'static str],
12    /// The macOS uniform type identifier if one exists.
13    pub uti: Option<&'static str>,
14    /// The MIME type if one exists.
15    pub mime: Option<&'static str>,
16}
17
18/// Returns the matching file type index for a path.
19pub fn file_type_index_for_path(path: &Path, file_types: &[FileType]) -> Option<usize> {
20    let extension = path.extension()?.to_str()?.to_ascii_lowercase();
21    file_types.iter().position(|file_type| {
22        file_type
23            .extensions
24            .iter()
25            .map(|candidate| candidate.trim_start_matches('.'))
26            .any(|candidate| !candidate.is_empty() && candidate.eq_ignore_ascii_case(&extension))
27    })
28}
29
30/// Returns the default file type index when any file types are configured.
31pub fn default_file_type_index(file_types: &[FileType]) -> Option<usize> {
32    (!file_types.is_empty()).then_some(0)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    const TYPES: &[FileType] = &[FileType {
40        name: "Text",
41        extensions: &["txt", ".md", ""],
42        uti: None,
43        mime: Some("text/plain"),
44    }];
45
46    #[test]
47    fn extension_matching_is_case_insensitive_and_accepts_leading_dots() {
48        assert_eq!(
49            file_type_index_for_path(Path::new("README.MD"), TYPES),
50            Some(0)
51        );
52        assert_eq!(
53            file_type_index_for_path(Path::new("notes.TXT"), TYPES),
54            Some(0)
55        );
56        assert_eq!(
57            file_type_index_for_path(Path::new("trailing."), TYPES),
58            None
59        );
60    }
61}