Ibis/src/courses.rs

213 lines
6.1 KiB
Rust

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<String>,
#[serde(default)]
#[structopt(long)]
moodle: Option<String>,
#[serde(deserialize_with = "string_or_vec", alias = "semester")]
semesters: Vec<String>,
#[structopt(skip)]
meetings: Option<Vec<Meeting>>,
}
#[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<std::path::Path> + 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<String> {
&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<Vec<Meeting>> {
&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<CourseList> {
let mut courses: Vec<Course> = 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<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
match StringOrVec::deserialize(deserializer)? {
StringOrVec::String(s) => Ok(vec![s]),
StringOrVec::Vec(vec) => Ok(vec),
}
}
pub struct CourseList(Vec<Course>);
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<Course>) -> Self {
CourseList(inner)
}
pub fn iter(&self) -> std::slice::Iter<Course> {
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<ParseIntError> for ParseSemesterError{
// fn from(e: ParseIntError) -> Self {
// Self::ParseIntError(e)
// }
// }