Skip to main content

superconf/
lib.rs

1//! A barebones configuration file made for low-dependency rust applications.
2//!
3//! # Usage
4//!
5//! Add to your `Cargo.toml` file:
6//!
7//! ```toml
8//! [dependancies]
9//! superconf = "0.3"
10//! ```
11//!
12//! # Examples
13//!
14//! Default seperator (space ` `) demonstration:
15//!
16//! ```rust
17//! use superconf::parse;
18//!
19//! let input = "my_key my_value";
20//!
21//! println!("Outputted HashMap: {:#?}", parse(input).unwrap());
22//! ```
23//!
24//! Or if you'd like to use a custom seperator like `:` or `=`:
25//!
26//! ```rust
27//! use superconf::parse_custom_sep;
28//!
29//! let input_equal = "custom=seperator";
30//! let input_colon = "second:string";
31//!
32//! println!("Equals seperator: {:#?}", parse_custom_sep(input_equal, '=').unwrap());
33//! println!("Colon seperator: {:#?}", parse_custom_sep(input_colon, ':').unwrap());
34//! ```
35//!
36//! Here is a complete syntax demonstration:
37//!
38//! ```none
39//! # comments are like this
40//! # no seperators are allowed in keys or values
41//! # comments can only be at the start of lines, no end of line comments here
42//!
43//! # my_key is the key, my_value is the value
44//! my_key the_value
45//!
46//! # you can use seperators as plaintext, just have to be backslashed
47//! your_path /home/user/Cool\ Path/x.txt
48//!
49//! # you can also have multiple levels
50//! # will be:
51//! # {"other_key": {"in_level": "see_it_is", "second_level": {"another": "level"}}}
52//! other_key
53//!     in_level see_it_is
54//!     second_level
55//!         another level
56//! ```
57//!
58//! # Config Conventions
59//!
60//! Some conventions commonly used for superconf files:
61//!
62//! - The file naming scheme is `snake_case`
63//! - All superconf files should end in the `.super` file extension
64//! - Try to document each line with a comment
65//! - If commented, space each config part with an empty line seperating it from
66//! others. If it is undocumented, you may bunch all config parts together
67//!
68//! # Motives
69//!
70//! Made this as a quick custom parser to challenge myself a bit and to use for
71//! a quick-n-dirty configuration format in the future. It's not the best file
72//! format in the world but it gets the job done.
73
74use std::collections::HashMap;
75use std::fs::File;
76use std::io::prelude::*;
77use std::path::PathBuf;
78
79/// Primary error enum for superconf, storing the common errors faced.
80#[derive(Debug)]
81pub enum SuperError {
82    /// When a line had a key but no value, e.g. `my_key`
83    NoKey,
84
85    /// When a line had a value but no key. This should ususally not happen when
86    /// parsing due to the nature of the library.
87    NoValue,
88
89    /// When adding elements and two are named with the same key, e.g:
90    ///
91    /// ```superconf
92    /// my_value original
93    /// my_value this_will_error
94    /// ```
95    ElementExists(SuperValue),
96
97    /// An IO error stemming from [parse_file].
98    IOError(std::io::Error),
99}
100
101/// The possible value of the config file.
102///
103/// Ususally a [SuperValue::Single] if 1 is provided or [SuperValue::List] if 2
104/// or more are.
105#[derive(Debug, Clone, PartialEq, PartialOrd)]
106pub enum SuperValue {
107    /// A single element provided, e.g. `my_key element`
108    Single(String),
109
110    /// Multiple elements provided, e.g. `my_key first_element second_element`
111    List(Vec<String>),
112}
113
114/// The type of token for the mini lexer
115#[derive(Debug, Clone, PartialEq, PartialOrd)]
116enum TokenType {
117    Character(char),
118    Seperator,
119    Backslash,
120    Comment,
121}
122
123/// Lexes input into [Vec]<[Vec]<[TokenType]>> (top level for line, 2nd level
124/// for each char in line).
125fn lex_str(conf: &str, seperator: char) -> Vec<Vec<TokenType>> {
126    let mut output: Vec<Vec<TokenType>> = vec![];
127
128    for line in conf.lines() {
129        let mut buffer: Vec<TokenType> = vec![];
130
131        for line_char in line.chars() {
132            let got_token = if line_char == seperator {
133                TokenType::Seperator
134            } else {
135                match line_char {
136                    '#' => TokenType::Comment,
137                    '\\' => TokenType::Backslash,
138                    t => TokenType::Character(t),
139                }
140            };
141
142            buffer.push(got_token);
143        }
144
145        output.push(buffer);
146    }
147
148    output
149}
150
151/// Similar to [parse] but can enter a custom seperator other then the
152/// default ` ` (space) character
153pub fn parse_custom_sep(
154    conf: &str,
155    seperator: char,
156) -> Result<HashMap<String, SuperValue>, SuperError> {
157    let mut output: HashMap<String, SuperValue> = HashMap::new();
158    let tokens = lex_str(conf, seperator);
159
160    let mut _expect_new_level = false; // if only 1 key was found in line, expect a new level
161
162    for token_line in tokens {
163        // TODO: use `_expect_new_level` up here
164
165        let mut buffer = vec![String::new()];
166
167        let mut ignore_special = false; // a catcher for special chars prefixed with 1 `\`
168        let mut is_comment = false; // used for skipping appends to output
169
170        for token in token_line {
171            match token {
172                TokenType::Comment => {
173                    // say its a comment then exit line
174                    is_comment = true;
175                    break;
176                }
177                TokenType::Backslash => ignore_special = !ignore_special, // switch ignore special
178                TokenType::Seperator => {
179                    // handle a seperator, ensuring that `\`'s are handled
180                    if ignore_special {
181                        // add seperator to buffer if it was backslashed
182                        buffer.last_mut().unwrap().push(seperator);
183
184                        ignore_special = false;
185                    } else if !buffer.last().unwrap().is_empty() {
186                        // add new string to value buffer
187                        buffer.push(String::new())
188                    }
189                }
190                TokenType::Character(c) => buffer.last_mut().unwrap().push(c),
191            }
192        }
193
194        if is_comment {
195            continue;
196        }
197
198        let key = buffer.remove(0);
199
200        let final_value = match buffer.len() {
201            0 => {
202                _expect_new_level = true;
203                continue;
204            } // looks to be a new level, expect it
205            1 => SuperValue::Single(buffer[0].clone()),
206            _ => SuperValue::List(buffer),
207        };
208
209        match output.insert(key, final_value) {
210            Some(element) => return Err(SuperError::ElementExists(element)),
211            None => (),
212        };
213    }
214
215    Ok(output)
216}
217
218/// Parses given `conf` input.
219pub fn parse(conf: impl AsRef<str>) -> Result<HashMap<String, SuperValue>, SuperError> {
220    parse_custom_sep(conf.as_ref(), ' ')
221}
222
223/// Opens a [PathBuf]-type file and parses contents.
224pub fn parse_file(conf_path: PathBuf) -> Result<HashMap<String, SuperValue>, SuperError> {
225    let mut file = match File::open(conf_path) {
226        Ok(f) => f,
227        Err(e) => return Err(SuperError::IOError(e)),
228    };
229
230    let mut contents = String::new();
231
232    match file.read_to_string(&mut contents) {
233        Ok(_) => parse(contents),
234        Err(e) => Err(SuperError::IOError(e)),
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    /// Tests basic parsing capabilities of just 1 key-value
243    #[test]
244    fn basic_parse() {
245        let input = "my_key my_value";
246
247        parse(input).unwrap();
248    }
249
250    /// Tests that comments are working properly
251    #[test]
252    fn comment_test() {
253        let input = "# This is a line comment, should not return any output!";
254
255        assert_eq!(HashMap::new(), parse(input).unwrap());
256    }
257
258    /// Tests valid keys that include backstroke seperators as a torture test
259    #[test]
260    fn backstroke_seperator_torture() {
261        let input = "my\\ key this\\ is\\ the\\ value";
262        let mut exp_output = HashMap::new();
263        exp_output.insert(
264            "my key".to_string(),
265            SuperValue::Single("this is the value".to_string()),
266        );
267
268        assert_eq!(exp_output, parse(input).unwrap())
269    }
270
271    /// Tests a realistic-looking path
272    #[test]
273    fn realistic_path() {
274        let input = "your_path /home/user/Cool\\ Path/x.txt";
275
276        let mut exp_output = HashMap::new();
277        exp_output.insert(
278            "your_path".to_string(),
279            SuperValue::Single("/home/user/Cool Path/x.txt".to_string()),
280        );
281
282        assert_eq!(exp_output, parse(input).unwrap());
283    }
284
285    /// Tests that eol comments like  the `# hi` in:
286    ///
287    /// ```superconf
288    /// my_key my_value # hi
289    /// ```
290    ///
291    /// Work properly.
292    #[test]
293    fn eol_comment() {
294        let input = "my_key my_value # eol comment";
295
296        parse(input).unwrap();
297    }
298
299    /// Tests that lists properly work
300    #[test]
301    fn item_list() {
302        let input = "my_key first_val second_val";
303
304        let mut exp_out = HashMap::new();
305        exp_out.insert(
306            "my_key".to_string(),
307            SuperValue::List(vec!["first_val".to_string(), "second_val".to_string()]),
308        );
309        assert_eq!(exp_out, parse(input).unwrap());
310    }
311
312    /// Ensures custom seperators are properly parsed
313    #[test]
314    fn custom_seperator() {
315        let input = "arrow>demonstration";
316
317        let mut exp_out = HashMap::new();
318        exp_out.insert(
319            "arrow".to_string(),
320            SuperValue::Single(String::from("demonstration")),
321        );
322        assert_eq!(exp_out, parse_custom_sep(input, '>').unwrap());
323    }
324
325    #[test]
326    fn custom_seperator_space() {
327        let mut output: HashMap<String, SuperValue> = HashMap::new();
328        output.insert(
329            "hi".into(),
330            SuperValue::Single("is cool?: no..".to_string()),
331        );
332
333        assert_eq!(
334            parse_custom_sep("hi:is cool?\\: no..", ':').unwrap(),
335            output
336        )
337    }
338}