easy_configuration_format/
lib.rs1#![warn(missing_docs)]
103
104
105
106pub mod data;
108pub use data::*;
109pub mod utils;
111pub use utils::*;
112
113
114
115use std::collections::{HashMap, HashSet};
116
117
118
119
120
121pub fn parse_settings<T>(contents: impl AsRef<str>, updater_fns: &[fn(&mut HashMap<String, Value>, &T)], args: &T) -> (File, Vec<ParseEntryError>) {
125 let mut layout = vec!();
126 let mut values = HashMap::new();
127 let mut errors = vec!();
128
129 let lines = contents.as_ref().split('\n').collect::<Vec<_>>();
130 let version = get_file_version(lines[0].trim());
131 let mut line_i = 1;
132 loop {
133 let result = parse_line(&lines, &mut line_i, &mut layout, &mut values);
134 if let Err(err) = result {
135 layout.push(LayoutEntry::Comment (lines[line_i].to_string()));
136 errors.push(err);
137 }
138 line_i += 1;
139 if line_i >= lines.len() {break;}
140 }
141
142 if let Some(version) = version {
143 for updater_fn in &updater_fns[version - 1 ..] {
144 (updater_fn)(&mut values, args);
145 }
146 } else {
147 errors.push(ParseEntryError::new(0, "Could not find version, assuming version is latest"));
148 }
149
150 (File {
151 values,
152 layout,
153 version: updater_fns.len() + 1,
154 }, errors)
155}
156
157
158
159fn get_file_version(first_line: &str) -> Option<usize> {
160 let Some(format_str) = first_line.strip_prefix("format ") else {return None;};
161 format_str.parse::<usize>().ok()
162}
163
164
165
166fn parse_line(
167 lines: &[&str],
168 line_i: &mut usize,
169 layout: &mut Vec<LayoutEntry>,
170 values: &mut HashMap<String, Value>,
171) -> Result<(), ParseEntryError> {
172
173 let line_trimmed = lines[*line_i].trim();
174 if line_trimmed.is_empty() {
175 layout.push(LayoutEntry::Empty);
176 return Ok(());
177 }
178
179 if line_trimmed == "##" {
180 layout.push(parse_multiline_comment(lines, line_i)?);
181 return Ok(());
182 }
183 if let Some(comment) = line_trimmed.strip_prefix("#") {
184 layout.push(LayoutEntry::Comment (comment.to_string()));
185 return Ok(());
186 }
187
188 let colon_index = line_trimmed.find(':');
189 let Some(colon_index) = colon_index else {return Err(ParseEntryError::new(*line_i, "No colon (':') was found, either add a colon after the key or mark this as a comment."));};
190 if colon_index == 0 {return Err(ParseEntryError::new(*line_i, "Lines cannot start with a colon."));}
191 let key = &line_trimmed[..colon_index];
192 if values.contains_key(key) {return Err(ParseEntryError::new(*line_i, format!("Key \"{key}\" is already defined.")));}
193 let value = parse_value(lines, line_i, colon_index)?;
194 layout.push(LayoutEntry::Key (key.to_string()));
195 values.insert(key.to_string(), value);
196
197 Ok(())
198}
199
200
201
202fn parse_multiline_comment(
203 lines: &[&str],
204 line_i: &mut usize,
205) -> Result<LayoutEntry, ParseEntryError> {
206
207 let start_line_i = *line_i;
208 let mut output = String::new();
209 *line_i += 1;
210 while lines[*line_i].trim() != "##" {
211 output += lines[*line_i];
212 output.push('\n');
213 *line_i += 1;
214 if *line_i == lines.len() {
215 *line_i = start_line_i;
216 return Err(ParseEntryError::new(start_line_i, "Could not find an end of this multiline comment. To end a multiline comment, its last line should be nothing but '##'."));
217 }
218 }
219 output.pop();
220 Ok(LayoutEntry::Comment (output))
221}
222
223
224
225fn parse_value(lines: &[&str], line_i: &mut usize, colon_index: usize) -> Result<Value, ParseEntryError> {
226 let line_trimmed = lines[*line_i].trim();
227
228 let value_start_i =
229 line_trimmed.char_indices()
230 .skip(colon_index + 1)
231 .find(|(_i, c)| !c.is_whitespace());
232 let Some((value_start_i, _c)) = value_start_i else {return Err(ParseEntryError::new(*line_i, "No value was found for this key (if this is meant to be empty, please set the value as 'empty')."));};
233
234 let value = &line_trimmed[value_start_i..];
235 match &*value.to_lowercase() {
236 "empty" => return Ok(Value::Empty),
237 "true" => return Ok(Value::Bool (true)),
238 "false" => return Ok(Value::Bool (false)),
239 "\"" => return parse_multiline_string(lines, line_i),
240 _ => {}
241 }
242 let first_char = value.chars().next().unwrap(); if first_char.is_ascii_digit() {
244 if let Ok(i64_value) = value.parse::<i64>() {return Ok(Value::I64 (i64_value));}
245 if let Ok(f64_value) = value.parse::<f64>() {return Ok(Value::F64 (f64_value));}
246 }
247 if first_char == '"' {
248 let last_char = value.chars().last().unwrap(); if last_char != '"' {return Err(ParseEntryError::new(*line_i, "Invalid string, no ending quote found. If this is a single-line string, no characters are allowed after the final quotation mark. If this is meant to be a multi-line string, no characters are allowed after the first quotation mark."))}
250 return Ok(Value::String (value[1 .. value.len()-1].to_string()));
251 }
252
253 Err(ParseEntryError::new(*line_i, "Invalid value, must be 'empty', 'true', 'false', a valid integer, a valid decimal number, a string enclosed in quotes, or a multiline quote starting with a single '\"' character."))
254}
255
256
257
258fn parse_multiline_string(lines: &[&str], line_i: &mut usize) -> Result<Value, ParseEntryError> {
259 let mut output = String::new();
260 let start_i = *line_i;
261 *line_i += 1;
262 let mut curr_line = lines[*line_i].trim_start();
263 while curr_line.starts_with('"') {
264 output += &curr_line[1..];
265 output.push('\n');
266 *line_i += 1;
267 if *line_i == lines.len() {break;}
268 curr_line = lines[*line_i].trim_start();
269 }
270 *line_i -= 1;
271 output.pop();
272 if *line_i == start_i {
273 return Err(ParseEntryError::new(start_i, String::from("Invalid value, multiline strings cannot be empty")));
274 }
275 Ok(Value::String (output))
276}
277
278
279
280
281
282pub fn format_settings(file: &File) -> (String, Vec<FormatEntryError>) {
284 let mut output = format!("format {}\n", file.version);
285 if file.layout.is_empty() {return (output, vec!());}
286 let mut errors = vec!();
287 let mut printed_keys = HashSet::new();
288 for entry in &file.layout {
289 match entry {
290 LayoutEntry::Empty => {}
291 LayoutEntry::Comment (comment) => {
292 if comment.contains('\n') {
293 output += "##\n";
294 output += comment;
295 output += "\n##";
296 } else {
297 output.push('#');
298 output += comment;
299 }
300 }
301 LayoutEntry::Key (key) => {
302 output += key;
303 output += ": ";
304 let value = file.get(key);
305 if let Some(value) = value {
306 output += &value.format();
307 } else {
308 errors.push(FormatEntryError::new(key));
309 continue;
310 };
311 printed_keys.insert(key.to_string());
312 }
313 }
314 output.push('\n');
315 }
316 for (key, value) in &file.values {
317 if printed_keys.contains(key) {continue;}
318 output += key;
319 output += ": ";
320 output += &value.format();
321 output.push('\n');
322 }
323 output.pop();
324 (output, errors)
325}
326
327
328
329
330
331pub fn merge_values(existing_values: &mut HashMap<String, Value>, new_values: &HashMap<String, Value>, merge_options: MergeOptions) {
333 match merge_options {
334 MergeOptions::UpdateOnly => {
335 for (key, value) in new_values {
336 if existing_values.contains_key(key) {
337 existing_values.insert(key.clone(), value.clone());
338 }
339 }
340 }
341 MergeOptions::UpdateAndAdd => {
342 for (key, value) in new_values {
343 existing_values.insert(key.clone(), value.clone());
344 }
345 }
346 MergeOptions::AddOnly => {
347 for (key, value) in new_values {
348 if !existing_values.contains_key(key) {
349 existing_values.insert(key.clone(), value.clone());
350 }
351 }
352 }
353 MergeOptions::FullyReplace => {
354 *existing_values = new_values.clone();
355 }
356 }
357}
358
359pub enum MergeOptions {
361 UpdateOnly,
363 UpdateAndAdd,
365 AddOnly,
367 FullyReplace,
369}