use crate::{model::Line, util::serialization}; use anyhow::{Error, Result}; use std::fmt; use std::fs::File; use std::io::{prelude::*, BufReader}; use std::path::Path; #[derive(Debug, Clone)] struct ParseError { line: usize, message: String, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Line {}: {}", self.line, self.message) } } impl std::error::Error for ParseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } pub fn read_file(path: &str) -> Result> { let file = File::open(path)?; let reader = BufReader::new(file); let mut entries: Vec = Vec::new(); for (index, line) in reader.lines().enumerate() { let line = line?; if let Some(line) = read_line(index, &line)? { entries.push(line) } } Ok(entries) } fn read_line(index: usize, line: &str) -> Result> { let line = line.trim(); if line.starts_with('#') || line.is_empty() { Ok(None) } else if !line.starts_with('-') { Err(Error::from(ParseError { line: index + 1, message: "an entry should starts with “-”.".to_string(), })) } else { let without_minus = line.split('-').skip(1).collect::>().join("-"); let without_comment = without_minus.split('#').collect::>()[0].trim(); let translation = without_comment.split(':').collect::>(); if translation.len() != 2 { Err(Error::from(ParseError { line: index + 1, message: "an entry should contain two parts separated by “:”.".to_string(), })) } else { let t1 = translation[0].trim(); let t2 = translation[1].trim(); if t1.is_empty() || t2.is_empty() { Err(Error::from(ParseError { line: index + 1, message: "an entry should contain two parts separated by “:”.".to_string(), })) } else { Ok(Some(Line { part_1: serialization::line_to_words(t1), part_2: serialization::line_to_words(t2), })) } } } } pub fn pp_from_path(path: &str) -> Option { Some(capitalize( Path::new(&path).with_extension("").file_name()?.to_str()?, )) } fn capitalize(s: &str) -> String { let mut c = s.chars(); match c.next() { None => String::new(), Some(f) => f.to_uppercase().collect::() + c.as_str(), } } #[cfg(test)] pub mod tests { use crate::model::Line; use anyhow::Result; #[test] fn errors() { is_error("A : a"); is_error("- A"); is_error("- A -> a"); is_error("- A : B : C"); is_error("- : "); is_error("- A : a\n-") } #[test] fn ignored() { check("", &[]); check(" ", &[]); check(" \n \n ", &[]); check("# 1", &[]); check("# 1\n\n # 2", &[]); } #[test] fn card() { check("- A : a", &[(&["A"], &["a"])]); } #[test] fn cards() { check("- A : a\n- B : b", &[(&["A"], &["a"]), (&["B"], &["b"])]); } #[test] fn alternatives() { check("- A : a1 | a2", &[(&["A"], &["a1", "a2"])]); check("- A1 | A2 : a", &[(&["A1", "A2"], &["a"])]); check("- A1 | A2 : a1 | a2", &[(&["A1", "A2"], &["a1", "a2"])]); } fn is_error(content: &str) { assert!(read_string(content).is_err()) } fn check(content: &str, res: &[(&[&str], &[&str])]) { assert_eq!( read_string(content).unwrap(), res.iter() .map(|(part_1, part_2)| Line { part_1: part_1.iter().map(|x| x.to_string()).collect::>(), part_2: part_2.iter().map(|x| x.to_string()).collect::>() }) .collect::>() ) } pub fn read_string(content: &str) -> Result> { let mut entries: Vec = Vec::new(); for (index, line) in content.lines().enumerate() { if let Some(line) = super::read_line(index, line)? { entries.push(line) } } Ok(entries) } }