use crate::config::{ListOptions, CONFIG}; use crate::util; use anyhow::Result; use glob::glob; use serde::{Deserialize, Deserializer, Serialize}; use std::io::prelude::*; use std::os::unix::fs::symlink; use std::{ fs::{remove_file, File}, path::PathBuf, }; use structopt::StructOpt; #[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] pub struct Course { pub name: String, #[serde(skip)] #[structopt(skip)] pub path: PathBuf, short: String, #[serde(default)] #[structopt(long)] url: Option, #[serde(default)] #[structopt(long)] moodle: Option, #[serde(deserialize_with = "string_or_vec", alias = "semester")] semesters: Vec, #[structopt(skip)] meetings: Option>, } #[derive(Serialize, Deserialize, StructOpt, Debug, Clone)] pub struct Meeting { pub name: String, pub url: String, } impl Course { pub fn current() -> Self { Self::from_path(&CONFIG.link_path) } // TODO: make this the single method to read a Course pub fn from_path(path: impl AsRef + Clone) -> Self { // TODO: make "course.toml" a *documented* magic value or configurable let path: PathBuf = path.as_ref().join("course.toml"); let course_string = std::fs::read_to_string(&path); if let Ok(course_string) = course_string { let mut course: Course = toml::from_str(&course_string).unwrap_or_else(|e| { panic!("Error while parsing course at {:?} \n{}", &path, e); }); course.path = path; course } else { panic!( "Error while reading course at {:?}\n{}", path, course_string.unwrap_err() ); } } /// Get a reference to the course's semesters. pub fn semesters(&self) -> &Vec { &self.semesters } /// Get a reference to the course's semesters. pub fn semesters_mut(&mut self) -> &mut Vec { &mut self.semesters } /// Check if course is part of current semester pub fn is_current(&self) -> bool { self.semesters() .iter() .any(|x| x == &util::get_current_semester()) } pub fn meetings(&self) -> &Option> { &self.meetings } pub fn set(&self) -> Result<()> { // TODO: make sure this is actually a link, and we are not removing some other file of the user remove_file(&CONFIG.link_path)?; symlink(&self.path, &CONFIG.link_path)?; Ok(()) } pub fn list(options: &ListOptions) -> Result { let mut courses: Vec = vec![]; let pattern = [ CONFIG.files_root.canonicalize().unwrap().to_str().unwrap(), "/*/course.toml", ] .concat(); let mut handle; for entry in glob(pattern.as_str()).unwrap() { let path = entry.unwrap(); let mut coursestring = String::new(); handle = File::open(&path).unwrap(); handle.read_to_string(&mut coursestring).unwrap(); let mut course: Course = toml::from_str(&coursestring)?; course.path = path.parent().unwrap().to_path_buf(); if options.all || course.is_current() { courses.push(course); } } Ok(CourseList::new(courses)) } pub fn check(list_options: &ListOptions) -> Result<()> { let mut problematic_course_count = 0; let toml_list = Self::list(list_options)?; println!("Checking course.toml's"); for c in toml_list.iter() { let mut problems = Vec::new(); if c.name.is_empty() { problems.push(" Name is empty") } if c.short.is_empty() { problems.push(" Short is empty") } if c.url.is_none() { problems.push(" No URL (set empty string to mark nonexistant website)") } if c.moodle.is_none() { problems.push(" No Moodle-URL (set empty string to mark nonexistant website)") } if c.semesters.is_empty() { problems.push(" Course is not in any semesters") } if !problems.is_empty() { eprintln!( "Found {} problems with {}/course.toml", problems.len(), c.path.to_str().unwrap() ); for p in problems { eprintln!("{}", p) } problematic_course_count += 1; } else { println!("No problems found with {:?}", c.path); } } println!("Found {} courses with problems", problematic_course_count); Ok(()) } } fn string_or_vec<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrVec { String(String), Vec(Vec), } match StringOrVec::deserialize(deserializer)? { StringOrVec::String(s) => Ok(vec![s]), StringOrVec::Vec(vec) => Ok(vec), } } pub struct CourseList(Vec); impl std::fmt::Display for CourseList { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for c in &self.0 { writeln!(f, "{}", c)?; } Ok(()) } } impl CourseList { pub fn new(inner: Vec) -> Self { CourseList(inner) } pub fn iter(&self) -> std::slice::Iter { self.0.iter() } pub fn get(&self, element: usize) -> Option<&Course> { self.0.get(element) } } impl std::fmt::Display for Course { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) } } // #[derive(Debug)] // pub enum ParseSemesterError{ // ParseIntError(ParseIntError), // NoFormatFound, // } // impl std::fmt::Display for ParseSemesterError{ // fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // todo!() // } // } // impl std::convert::From for ParseSemesterError{ // fn from(e: ParseIntError) -> Self { // Self::ParseIntError(e) // } // }