Skip to main content

fancy_tree/sorting/
directories.rs

1//! Module for how to include directories in sorting.
2use mlua::{FromLua, Lua};
3use std::cmp::Ordering;
4use std::path::Path;
5
6/// How directories should be included in sorting.
7#[derive(Debug, PartialEq, Eq, Clone, Copy)]
8pub enum Directories {
9    /// Directories and files should be mixed together.
10    Mixed,
11    /// Directories should come first.
12    First,
13    /// Directories should be last.
14    Last,
15}
16
17impl Directories {
18    const MIXED_NAME: &'static str = "mixed";
19    const FIRST_NAME: &'static str = "first";
20    const LAST_NAME: &'static str = "last";
21
22    /// Converts a string to `Self`.
23    fn from_string(s: &str) -> Option<Self> {
24        use Directories::*;
25
26        [
27            (Self::MIXED_NAME, Mixed),
28            (Self::FIRST_NAME, First),
29            (Self::LAST_NAME, Last),
30        ]
31        .into_iter()
32        .find_map(|(name, d)| (s == name).then_some(d))
33    }
34
35    /// Compares two paths and provides the proper ordering if they are directories or not.
36    pub fn cmp<L, R>(&self, left: L, right: R) -> Ordering
37    where
38        L: AsRef<Path>,
39        R: AsRef<Path>,
40    {
41        self.cmp_impl(left.as_ref(), right.as_ref())
42    }
43
44    /// Implementation for comparison.
45    fn cmp_impl<L, R>(&self, left: L, right: R) -> Ordering
46    where
47        L: IsDirectory,
48        R: IsDirectory,
49    {
50        if let Self::Mixed = self {
51            // NOTE Whether or not they are directories don't matter at all. Small
52            //      optimization to avoid calling path methods.
53            return Ordering::Equal;
54        }
55
56        match (self, left.is_directory(), right.is_directory()) {
57            (Self::Mixed, _, _) => unreachable!("Already checked for mixed ordering"),
58            (_, true, true) | (_, false, false) => Ordering::Equal,
59            (Self::First, true, false) | (Self::Last, false, true) => Ordering::Less,
60            (Self::First, false, true) | (Self::Last, true, false) => Ordering::Greater,
61        }
62    }
63}
64
65impl Default for Directories {
66    #[inline]
67    fn default() -> Self {
68        Self::Mixed
69    }
70}
71
72impl FromLua for Directories {
73    fn from_lua(value: mlua::Value, lua: &Lua) -> mlua::Result<Self> {
74        let type_name = value.type_name();
75
76        let conversion_error = || {
77            let choices = [Self::MIXED_NAME, Self::FIRST_NAME, Self::LAST_NAME].join(", ");
78
79            mlua::Error::FromLuaConversionError {
80                from: type_name,
81                to: String::from("Directories"),
82                message: Some(choices),
83            }
84        };
85
86        let s = String::from_lua(value, lua)?;
87        Self::from_string(&s).ok_or_else(conversion_error)
88    }
89}
90
91/// Trait for types to check if they are directories.
92trait IsDirectory {
93    /// Returns true if the thing is a directory.
94    fn is_directory(&self) -> bool;
95}
96
97impl IsDirectory for &Path {
98    #[inline]
99    fn is_directory(&self) -> bool {
100        self.is_dir()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use rstest::rstest;
108
109    #[rstest]
110    #[case(r#""mixed""#, Directories::Mixed)]
111    #[case(r#""first""#, Directories::First)]
112    #[case(r#""last""#, Directories::Last)]
113    fn test_from_lua(#[case] chunk: &str, #[case] expected: Directories) {
114        let lua = Lua::new();
115        let actual: Directories = lua.load(chunk).eval().unwrap();
116        assert_eq!(expected, actual);
117    }
118
119    #[test]
120    fn test_from_lua_err() {
121        let lua = Lua::new();
122        let chunk = r#"1"#;
123        assert!(lua.load(chunk).eval::<Directories>().is_err())
124    }
125
126    #[rstest]
127    #[case(Directories::Mixed, true, false, Ordering::Equal)]
128    #[case(Directories::Mixed, false, true, Ordering::Equal)]
129    #[case(Directories::First, true, true, Ordering::Equal)]
130    #[case(Directories::First, true, false, Ordering::Less)]
131    #[case(Directories::First, false, true, Ordering::Greater)]
132    #[case(Directories::Last, true, true, Ordering::Equal)]
133    #[case(Directories::Last, true, false, Ordering::Greater)]
134    #[case(Directories::Last, false, true, Ordering::Less)]
135    fn test_cmp(
136        #[case] directories: Directories,
137        #[case] left_is_dir: bool,
138        #[case] right_is_dir: bool,
139        #[case] expected: Ordering,
140    ) {
141        struct IsDir(bool);
142        impl IsDirectory for IsDir {
143            fn is_directory(&self) -> bool {
144                self.0
145            }
146        }
147
148        assert_eq!(
149            expected,
150            directories.cmp_impl(IsDir(left_is_dir), IsDir(right_is_dir))
151        )
152    }
153}