def/
lib.rs

1//! def is a library backing def command line tool. It mainly provides `Describer`
2//! structwhich is used to map string descriptions to paths and retrieve them when
3//! needed.
4
5use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9/// Directory seperator. Used to split a string.
10const SEPERATOR: char = '/';
11
12/// A place holder in patterns. Replaced with a name.
13const NAME_PLACEHOLDER: char = '*';
14
15/// Describer holds descriptions of files and directories.
16///
17/// # Types of Descriptions
18///
19/// - Specific description: A string mapped to a path describing a file or directory.
20/// When describe is called this will be retrieved as is.
21/// - Pattern description: A string mapped to a directory's path describing a child
22/// of the directory. When description of a child is wanted, the pattern is retrieved.
23/// In patterns, a wildcard is interpreted as a place holder for child's name, and are
24/// replaced by the name when retreived.
25///
26/// If a string can be described using both a pattern and a specific description,
27/// the specific description will be favoured.
28///
29/// # Examples
30///
31/// ```
32/// // Create a mutable describer.
33/// let mut describer = def::Describer::new();
34///
35/// // Map a description to a given path.
36/// describer.add_description("path/to/directory", "This is an empty directory.");
37///
38/// // Map a pattern to a given path. The pattern applies to the path's
39/// // children. "*" works as a placeholder and will be replaced by the
40/// // child's name.
41/// describer.add_pattern("parent/directory", "* is a child of parent/directory.");
42///
43/// // The description is retrieved as is.
44/// assert_eq!(
45///     describer.describe("path/to/directory"),
46///     Some("This is an empty directory.".to_string())
47/// );
48///
49/// // "*" is replaced with "test".
50/// assert_eq!(
51///     describer.describe("parent/directory/test"),
52///     Some("test is a child of parent/directory.".to_string())
53/// );
54///
55/// // Despite having a pattern mapped to it, the pattern only applies to
56/// // its children.
57/// assert_eq!(describer.describe("parent/directory"), None);
58/// ```
59///
60#[derive(Deserialize, Serialize, Debug)]
61pub struct Describer {
62    descriptions: HashMap<String, String>,
63    patterns: HashMap<String, String>,
64}
65
66impl Describer {
67    /// Create and return a new empty describer.
68    pub fn new() -> Describer {
69        Describer {
70            descriptions: HashMap::new(),
71            patterns: HashMap::new(),
72        }
73    }
74
75    /// Create and return a new describer using given HashMaps.
76    ///
77    /// # Arguments
78    ///
79    /// * `d` - A map of descriptions.
80    /// * `p` - A map of patterns.
81    pub fn new_with(d: HashMap<String, String>, p: HashMap<String, String>) -> Describer {
82        Describer {
83            descriptions: d,
84            patterns: p,
85        }
86    }
87
88    /// Create and return a new describer using the given JSON value.
89    ///
90    /// # Arguments
91    ///
92    /// * `json` - A string representing a JSON value that can be deserialized
93    /// into a Describer. An error is returned if the JSON string can't be
94    /// deserialized.
95    pub fn new_from_json(json: &str) -> Result<Describer, serde_json::Error> {
96        serde_json::from_str::<Describer>(json)
97    }
98
99    /// Return a description of the given path or None if no description
100    /// exists. The descriptions map is checked for a description first,
101    /// if none is found, then the patterns map is checked.
102    pub fn describe(&self, path: &str) -> Option<String> {
103        match self.descriptions.get(path) {
104            Some(d) => Some(d.clone()),
105            None => self.describe_using_pattern(path),
106        }
107    }
108
109    /// Check patterns map for a description. If one exists, return it with
110    /// all place holders replaced, otherwise return None.
111    fn describe_using_pattern(&self, path: &str) -> Option<String> {
112        let parent: Vec<&str> = path.rsplitn(2, SEPERATOR).collect();
113        if parent.len() != 2 {
114            None
115        } else {
116            match self.patterns.get(parent[1]) {
117                Some(p) => Some(p.replace(NAME_PLACEHOLDER, parent[0])),
118                None => None,
119            }
120        }
121    }
122
123    /// Add a description to the descriptions map.
124    pub fn add_description(&mut self, path: &str, desc: &str) {
125        self.descriptions.insert(path.to_string(), desc.to_string());
126    }
127
128    /// Add a pattern to the patterns map.
129    pub fn add_pattern(&mut self, path: &str, desc: &str) {
130        self.patterns.insert(path.to_string(), desc.to_string());
131    }
132
133    /// Return a string JSON representation of this Describer. This is
134    /// subsequently written to a file to be re-loaded on next run.
135    ///
136    /// # Arguments
137    ///
138    /// * `pretty` - If true, return a "pretty" JSON string.
139    pub fn to_json(&self, pretty: bool) -> Result<String, serde_json::Error> {
140        if pretty {
141            serde_json::to_string_pretty(self)
142        } else {
143            serde_json::to_string(self)
144        }
145    }
146}
147
148impl Default for Describer {
149    fn default() -> Describer {
150        Describer::new()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn new_describe_test() {
160        let mut descriptions: HashMap<String, String> = HashMap::new();
161        let mut patterns: HashMap<String, String> = HashMap::new();
162        for (path, desc, is_pattern) in [
163            ("/path/to/dir", "This is /path/to/dir.", false),
164            ("/another/dir", "This is /another/dir.", false),
165            ("/yet/another/path", "This is /yet/another/path.", false),
166            ("/path/to/dir", "* is in /path/to/dir.", true),
167            ("/yet/another/path", "* is in /yet/another/path.", true),
168            ("/obvious", "* is *", true),
169            ("/yet/another", "* is in /yet/another/path.", true),
170        ]
171        .iter()
172        {
173            if *is_pattern {
174                patterns.insert(path.to_string(), desc.to_string());
175            } else {
176                descriptions.insert(path.to_string(), desc.to_string());
177            }
178        }
179
180        describe_tester(&Describer::new_with(descriptions, patterns));
181    }
182
183    #[test]
184    fn new_from_json_describe_test() {
185        match Describer::new_from_json(
186            "
187	    {
188                \"descriptions\": {
189                        \"/path/to/dir\": \"This is /path/to/dir.\",
190                        \"/another/dir\": \"This is /another/dir.\",
191                        \"/yet/another/path\": \"This is /yet/another/path.\"
192		},
193                \"patterns\": {
194                        \"/path/to/dir\": \"* is in /path/to/dir.\",
195                        \"/yet/another/path\": \"* is in /yet/another/path.\",
196                        \"/obvious\": \"* is *\",
197                        \"/yet/another\": \"* is in /yet/another/path.\"
198                }
199            }",
200        ) {
201            Ok(d) => describe_tester(&d),
202            Err(e) => panic!(e),
203        };
204    }
205
206    #[test]
207    fn add_test() {
208        let mut d = Describer::new();
209        d.add_description("path/to/directory", "This is an empty directory.");
210        d.add_pattern("parent/directory", "* is a child of parent/directory.");
211        assert_eq!(
212            d.to_json(false).unwrap(),
213            format!(
214                "{}{}{}{}",
215                "{\"descriptions\":",
216                "{\"path/to/directory\":\"This is an empty directory.\"},",
217                "\"patterns\":",
218                "{\"parent/directory\":\"* is a child of parent/directory.\"}}"
219            )
220        );
221    }
222
223    fn describe_tester(describer: &Describer) {
224        for (path, desc, is_none) in [
225            ("/path/to/dir", "This is /path/to/dir.", false),
226            ("/another/dir", "This is /another/dir.", false),
227            ("/yet/another/path", "This is /yet/another/path.", false),
228            ("/path/to/dir/1", "1 is in /path/to/dir.", false),
229            ("/path/to/dir/things", "things is in /path/to/dir.", false),
230            ("/yet/another/path/1", "1 is in /yet/another/path.", false),
231            ("/yet/another/path/$", "$ is in /yet/another/path.", false),
232            ("/obvious/obviously", "obviously is obviously", false),
233            ("/doesn't/exist", "", true),
234        ]
235        .iter()
236        {
237            assert_eq!(
238                describer.describe(path),
239                if *is_none {
240                    None
241                } else {
242                    Some(desc.to_string())
243                }
244            );
245        }
246    }
247}