1use {
2 colored::*,
3 serde::{Deserialize, Serialize},
4 std::{
5 cmp::Ordering,
6 collections::BTreeMap,
7 error::Error,
8 ffi::OsString,
9 fs::File,
10 io::Write,
11 path::{Path, PathBuf},
12 },
13 structopt::StructOpt,
14 toml_edit::{Array, Decor, Document, InlineTable, Item, Table, Value},
15};
16
17pub type Res<T = ()> = Result<T, Box<dyn Error>>;
19
20pub fn run(mut opt: Opt, config: Config) -> Res {
21 let config: ProcessedConfig = config.into();
22
23 if opt.files.is_empty() && opt.folder.is_empty() {
24 opt.folder.push(std::env::current_dir()?);
25 }
26
27 for folder in opt.folder {
28 let files = find_files_recursively(folder, "toml", !opt.silent, &config.excludes);
29 opt.files.extend(files);
30 }
31
32 for file in opt.files {
33 config.process_file(file, opt.check, !opt.silent)?;
34 }
35
36 Ok(())
37}
38
39struct Entry<T> {
41 key: String,
42 value: T,
43 decor: Decor,
44}
45
46#[derive(StructOpt, Debug, Clone)]
47pub struct Opt {
48 #[structopt(name = "FILE", parse(from_os_str))]
52 pub files: Vec<PathBuf>,
53
54 #[structopt(long, parse(from_os_str))]
56 pub folder: Vec<PathBuf>,
57
58 #[structopt(short, long)]
61 pub check: bool,
62
63 #[structopt(short, long)]
65 pub silent: bool,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69pub struct GenericConfig<Keys> {
70 #[serde(default)]
74 pub keys: Keys,
75
76 #[serde(default)]
80 pub inline_keys: Keys,
81
82 #[serde(default)]
86 pub sort_arrays: bool,
87
88 #[serde(default)]
89 pub excludes: Vec<String>,
91}
92
93pub type Config = GenericConfig<Vec<String>>;
94pub type ProcessedConfig = GenericConfig<BTreeMap<String, usize>>;
95
96const CONFIG_FILE: &str = "toml-maid.toml";
97
98impl Config {
99 pub fn read_from_file() -> Option<Config> {
100 let mut path: PathBuf = std::env::current_dir().ok()?;
101 let filename = Path::new(CONFIG_FILE);
102
103 loop {
104 path.push(filename);
105
106 if path.is_file() {
107 let text = std::fs::read_to_string(&path).ok()?;
108 let config: Self = toml::from_str(&text).ok()?;
109 return Some(config);
110 }
111
112 if !(path.pop() && path.pop()) {
113 return None;
115 }
116 }
117 }
118}
119
120impl From<Config> for ProcessedConfig {
121 fn from(x: Config) -> Self {
122 let mut res = Self {
123 keys: BTreeMap::new(),
124 inline_keys: BTreeMap::new(),
125 sort_arrays: x.sort_arrays,
126 excludes: x.excludes,
127 };
128
129 for (i, key) in x.keys.iter().enumerate() {
130 res.keys.insert(key.clone(), i);
131 }
132
133 for (i, key) in x.inline_keys.iter().enumerate() {
134 res.inline_keys.insert(key.clone(), i);
135 }
136
137 res
138 }
139}
140
141fn absolute_path(path: impl AsRef<Path>) -> Res<String> {
142 Ok(std::fs::canonicalize(&path)?.to_string_lossy().to_string())
143}
144
145pub fn find_files_recursively(
146 dir_path: impl AsRef<Path>,
147 extension: &str,
148 verbose: bool,
149 excludes: &[String],
150) -> Vec<PathBuf> {
151 macro_rules! continue_on_err {
152 ($in:expr, $context:expr) => {
153 match $in {
154 Ok(e) => e,
155 Err(e) => {
156 if verbose {
157 eprintln!("Error while {}: {}", $context, e);
158 }
159 continue;
160 }
161 }
162 };
163 }
164
165 let dir_path: PathBuf = dir_path.as_ref().to_owned();
166 let mut matches = vec![];
167 let extension: OsString = extension.into();
168 let config_file: OsString = CONFIG_FILE.into();
169
170 let excludes: Vec<_> = excludes
171 .iter()
172 .map(|v| glob::Pattern::new(&v).expect("invalid pattern in 'excludes'"))
173 .collect();
174
175 for entry in ignore::WalkBuilder::new(&dir_path)
176 .skip_stdout(true)
177 .filter_entry(move |entry| {
178 let path = entry.path();
179 let relative_path = path
180 .strip_prefix(&dir_path)
181 .expect("scanned file should be inside scanned dir");
182
183 for exclude in &excludes {
184 if exclude.matches_path(&relative_path) {
185 return false;
186 }
187 }
188
189 true
190 })
191 .build()
192 {
193 let entry = continue_on_err!(entry, "getting file info");
194 let file_type =
195 continue_on_err!(entry.file_type().ok_or("no file type"), "getting file type");
196 let path = entry.path().to_owned();
197
198 if file_type.is_dir() {
200 continue;
201 }
202
203 if path.extension() != Some(&extension) {
205 continue;
206 }
207
208 if path.file_name() == Some(&config_file) {
211 continue;
212 }
213
214 matches.push(path);
215 }
216
217 matches
218}
219
220impl ProcessedConfig {
221 pub fn process_file(&self, path: impl AsRef<Path>, check: bool, verbose: bool) -> Res<()> {
223 let absolute_path = absolute_path(&path)?;
224 let text = std::fs::read_to_string(&path).unwrap_or_else(|e| {
225 eprintln!(
226 "Error while reading file \"{}\" : {}",
227 absolute_path,
228 e.to_string().red()
229 );
230 std::process::exit(3);
231 });
232
233 let doc = text.parse::<Document>()?;
234 let trailing = doc.trailing().trim_end();
235
236 let output_table = self.format_table(&doc)?;
237 let mut output_doc: Document = output_table.into();
238 output_doc.set_trailing(trailing); let output_text = format!("{}\n", output_doc.to_string().trim());
240
241 if check {
242 if text != output_text {
243 eprintln!("Check fails : {}", absolute_path.red());
244 std::process::exit(2);
245 } else if verbose {
246 println!("Check succeed: {}", absolute_path.green());
247 }
248 } else if text != output_text {
249 let mut file = File::create(&path)?;
250 file.write_all(output_text.as_bytes())?;
251 file.flush()?;
252 if verbose {
253 println!("Overwritten: {}", absolute_path.blue());
254 }
255 } else if verbose {
256 println!("Unchanged: {}", absolute_path.green());
257 }
258
259 Ok(())
260 }
261
262 fn format_table(&self, table: &Table) -> Res<Table> {
267 let mut formated_table = Table::new();
268 formated_table.set_implicit(true); let prefix = table.decor().prefix().unwrap_or("");
270 let suffix = table.decor().suffix().unwrap_or("");
271 formated_table.decor_mut().set_prefix(prefix);
272 formated_table.decor_mut().set_suffix(suffix);
273
274 let mut section_decor = Decor::default();
275 let mut section = Vec::<Entry<Item>>::new();
276
277 let sort = |x: &Entry<Item>, y: &Entry<Item>| {
278 let xord = self.keys.get(&x.key);
279 let yord = self.keys.get(&y.key);
280
281 match (xord, yord) {
282 (Some(_), None) => Ordering::Less,
283 (None, Some(_)) => Ordering::Greater,
284 (Some(x), Some(y)) => x.cmp(y),
285 (None, None) => x.key.cmp(&y.key),
286 }
287 };
288
289 for (i, (key, item)) in table.iter().enumerate() {
291 let mut key_decor = table.key_decor(key).unwrap().clone();
292
293 if i == 0 {
296 if let Some(prefix) = key_decor.prefix() {
297 if !prefix.is_empty() {
298 section_decor.set_prefix(prefix);
299 key_decor.set_prefix("".to_string());
300 }
301 }
302 }
303 else if let Some(prefix) = key_decor.prefix() {
307 if prefix.starts_with('\n') {
308 section.sort_by(sort);
310
311 for (i, mut entry) in section.into_iter().enumerate() {
312 if i == 0 {
314 if let Some(prefix) = section_decor.prefix() {
315 entry.decor.set_prefix(prefix);
316 }
317 }
318
319 formated_table.insert(&entry.key, entry.value);
320 *formated_table.key_decor_mut(&entry.key).unwrap() = entry.decor;
321 }
322
323 section = Vec::new();
325 section_decor = Decor::default();
326 section_decor.set_prefix(prefix);
327 key_decor.set_prefix("".to_string());
328 }
329 }
330
331 if let Some(suffix) = key_decor.suffix().map(|x| x.to_owned()) {
333 key_decor.set_suffix(suffix.trim_end_matches('\n'));
334 }
335
336 let new_item = match item {
338 Item::None => Item::None,
339 Item::Value(inner) => Item::Value(self.format_value(inner, false)?),
340 Item::Table(inner) => Item::Table(self.format_table(inner)?),
341 Item::ArrayOfTables(inner) => Item::ArrayOfTables(inner.clone()),
343 };
344
345 section.push(Entry {
346 key: key.to_string(),
347 value: new_item,
348 decor: key_decor,
349 });
350 }
351
352 section.sort_by(sort);
354
355 for (i, mut entry) in section.into_iter().enumerate() {
356 if i == 0 {
358 if let Some(prefix) = section_decor.prefix() {
359 entry.decor.set_prefix(prefix);
360 }
361 }
362
363 formated_table.insert(&entry.key, entry.value);
364 *formated_table.key_decor_mut(&entry.key).unwrap() = entry.decor;
365 }
366
367 Ok(formated_table)
368 }
369
370 pub fn format_inline_table(&self, table: &InlineTable, last: bool) -> Res<InlineTable> {
374 let mut formated_table = InlineTable::new();
375 if last {
376 formated_table.decor_mut().set_suffix(" ");
377 }
378
379 let mut entries = Vec::<Entry<Value>>::new();
380
381 let sort = |x: &Entry<Value>, y: &Entry<Value>| {
382 let xord = self.inline_keys.get(&x.key);
383 let yord = self.inline_keys.get(&y.key);
384
385 match (xord, yord) {
386 (Some(_), None) => Ordering::Less,
387 (None, Some(_)) => Ordering::Greater,
388 (Some(x), Some(y)) => x.cmp(y),
389 (None, None) => x.key.cmp(&y.key),
390 }
391 };
392
393 for (key, value) in table.iter() {
394 let mut key_decor = table.key_decor(key).unwrap().clone();
395
396 key_decor.set_prefix(" ");
398 key_decor.set_suffix(" ");
399
400 let new_value = value.clone();
401
402 entries.push(Entry {
403 key: key.to_string(),
404 value: new_value,
405 decor: key_decor,
406 });
407 }
408
409 entries.sort_by(sort);
410
411 let len = entries.len();
412 for (i, entry) in entries.into_iter().enumerate() {
413 let new_value = self.format_value(&entry.value, i + 1 == len)?;
414
415 formated_table.insert(&entry.key, new_value);
416 *formated_table.key_decor_mut(&entry.key).unwrap() = entry.decor;
417 }
418
419 Ok(formated_table)
420 }
421
422 pub fn format_value(&self, value: &Value, last: bool) -> Res<Value> {
424 Ok(match value {
425 Value::Array(inner) => Value::Array(self.format_array(inner, last)?),
426 Value::InlineTable(inner) => Value::InlineTable(self.format_inline_table(inner, last)?),
427 v => {
428 let mut v = v.clone();
429
430 let prefix = v.decor().prefix().map(|x| x.trim()).unwrap_or("");
432
433 let prefix = if prefix.is_empty() {
434 prefix.to_string()
435 } else {
436 format!(" {}", prefix)
437 };
438
439 let suffix = v.decor().suffix().map(|x| x.trim()).unwrap_or("");
440
441 let suffix = if suffix.is_empty() {
442 suffix.to_string()
443 } else {
444 format!(" {}", suffix)
445 };
446
447 let mut display = v.clone().decorated("", "").to_string();
452 if display.starts_with('\'')
453 && !display.starts_with("''")
454 && display.find(&['\\', '"'][..]).is_none()
455 {
456 if let Some(s) = display.strip_prefix('\'') {
457 display = s.to_string();
458 }
459
460 if let Some(s) = display.strip_suffix('\'') {
461 display = s.to_string();
462 }
463
464 v = display.into();
465 }
466
467 if last {
469 v.decorated(&format!("{} ", prefix), &format!("{} ", suffix))
470 } else {
471 v.decorated(&format!("{} ", prefix), &suffix.to_string())
472 }
473 }
474 })
475 }
476
477 fn format_array(&self, array: &Array, last: bool) -> Res<Array> {
483 let mut values: Vec<_> = array.iter().cloned().collect();
484
485 if self.sort_arrays {
486 values.sort_by(|x, y| match (x, y) {
487 (Value::String(x), Value::String(y)) => x.value().cmp(y.value()),
488 (Value::String(_), _) => Ordering::Less,
489 (_, Value::String(_)) => Ordering::Greater,
490 (_, _) => Ordering::Equal,
491 });
492 }
493
494 let mut new_array = Array::new();
495
496 for value in values.into_iter() {
497 new_array.push_formatted(value);
498 }
499
500 let mut multiline = array.trailing().starts_with('\n');
501 if !multiline {
502 for item in array.iter() {
503 if let Some(prefix) = item.decor().prefix() {
504 if prefix.contains('\n') {
505 multiline = true;
506 break;
507 }
508 }
509
510 if let Some(suffix) = item.decor().suffix() {
511 if suffix.contains('\n') {
512 multiline = true;
513 break;
514 }
515 }
516 }
517 }
518
519 if multiline {
521 let mut trailing = format!(
522 "{}\n",
523 array.trailing().trim_matches(&[' ', '\t'][..]).trim_end()
524 );
525
526 if !trailing.starts_with('\n') {
527 trailing = format!(" {trailing}");
528 }
529
530 new_array.set_trailing(&trailing);
531 new_array.set_trailing_comma(true);
532
533 for value in new_array.iter_mut() {
534 let prefix = value
535 .decor()
536 .prefix()
537 .unwrap_or("")
538 .trim_matches(&[' ', '\t'][..])
539 .trim_end_matches('\n');
540
541 let mut prefix = if !prefix.is_empty() {
542 format!("{}\n\t", prefix)
543 } else {
544 "\n\t".to_string()
545 };
546
547 if !prefix.starts_with('\n') {
548 prefix = format!(" {prefix}");
549 }
550
551 let mut suffix = value
552 .decor()
553 .suffix()
554 .unwrap_or("")
555 .trim_matches(&[' ', '\t', '\n'][..])
556 .to_string();
557
558 if !suffix.is_empty() {
561 suffix.push('\n');
562 }
563
564 let formatted_value = self.format_value(value, false)?;
565 *value = formatted_value.decorated(&prefix, &suffix);
566 }
567 }
568 else {
570 new_array.set_trailing("");
571 new_array.set_trailing_comma(false);
572
573 let len = new_array.len();
574 for (i, value) in new_array.iter_mut().enumerate() {
575 *value = self.format_value(value, i + 1 == len)?;
576 }
577 }
578
579 new_array.decor_mut().set_prefix(" ");
580 new_array
581 .decor_mut()
582 .set_suffix(if last { " " } else { "" });
583
584 Ok(new_array)
585 }
586}