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}