218 lines
6.3 KiB
Rust
218 lines
6.3 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
|
|
}
|
|
|
|
/// Get a reference to the course's semesters.
|
|
pub fn semesters_mut(&mut self) -> &mut Vec<String> {
|
|
&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<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)
|
|
// }
|
|
// }
|