use anyhow::{Context, Result, anyhow, ensure};
use parking_lot::RwLock;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::{
fs::File,
io::BufReader,
path::{self, Path, PathBuf},
sync::Arc,
};
use ustr::{Ustr, UstrMap, UstrSet};
use walkdir::WalkDir;
use crate::{
data::{
CourseManifest, ExerciseManifest, GenerateManifests, LessonManifest, NormalizePaths,
UnitType, UserPreferences,
},
graph::{InMemoryUnitGraph, UnitGraph},
};
pub const COURSE_MANIFEST_FILENAME: &str = "course_manifest.json";
pub const LESSON_MANIFEST_FILENAME: &str = "lesson_manifest.json";
pub const EXERCISE_MANIFEST_FILENAME: &str = "exercise_manifest.json";
pub trait CourseLibrary {
fn get_course_manifest(&self, course_id: Ustr) -> Option<Arc<CourseManifest>>;
fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<Arc<LessonManifest>>;
fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<Arc<ExerciseManifest>>;
fn get_course_ids(&self) -> Vec<Ustr>;
fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>>;
fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>>;
fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr>;
fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet;
}
pub(crate) trait GetUnitGraph {
fn get_unit_graph(&self) -> Arc<RwLock<InMemoryUnitGraph>>;
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializedCourseLibrary {
unit_graph: InMemoryUnitGraph,
course_map: UstrMap<CourseManifest>,
lesson_map: UstrMap<LessonManifest>,
exercise_map: UstrMap<ExerciseManifest>,
}
impl From<&LocalCourseLibrary> for SerializedCourseLibrary {
fn from(library: &LocalCourseLibrary) -> Self {
SerializedCourseLibrary {
unit_graph: (*library.unit_graph.read()).clone(),
course_map: library
.course_map
.iter()
.map(|(k, v)| (*k, (**v).clone()))
.collect(),
lesson_map: library
.lesson_map
.iter()
.map(|(k, v)| (*k, (**v).clone()))
.collect(),
exercise_map: library
.exercise_map
.iter()
.map(|(k, v)| (*k, (**v).clone()))
.collect(),
}
}
}
struct OpenCourseRequest {
course_root: PathBuf,
course_manifest: CourseManifest,
}
struct OpenCourseResult {
manifest: CourseManifest,
lessons: Vec<(LessonManifest, Vec<ExerciseManifest>)>,
}
pub struct LocalCourseLibrary {
pub unit_graph: Arc<RwLock<InMemoryUnitGraph>>,
pub course_map: UstrMap<Arc<CourseManifest>>,
pub lesson_map: UstrMap<Arc<LessonManifest>>,
pub exercise_map: UstrMap<Arc<ExerciseManifest>>,
pub user_preferences: UserPreferences,
}
impl LocalCourseLibrary {
fn open_manifest<T: DeserializeOwned>(path: &Path) -> Result<T> {
let display = path.display();
let file = File::open(path).context(format!("cannot open manifest file {display}"))?;
let reader = BufReader::new(file);
serde_json::from_reader(reader).context(format!("cannot parse manifest file {display}"))
}
fn get_file_name(path: &Path) -> Result<String> {
Ok(path
.file_name()
.ok_or(anyhow!("cannot get file name from DirEntry"))?
.to_str()
.ok_or(anyhow!("invalid dir entry {}", path.display()))?
.to_string())
}
#[cfg_attr(coverage, coverage(off))]
fn verify_exercise_manifest(
lesson_manifest: &LessonManifest,
exercise_manifest: &ExerciseManifest,
) -> Result<()> {
ensure!(!exercise_manifest.id.is_empty(), "ID in manifest is empty");
ensure!(
exercise_manifest.lesson_id == lesson_manifest.id,
"lesson_id in manifest for exercise {} does not match the manifest for lesson {}",
exercise_manifest.id,
lesson_manifest.id,
);
ensure!(
exercise_manifest.course_id == lesson_manifest.course_id,
"course_id in manifest for exercise {} does not match the manifest for course {}",
exercise_manifest.id,
lesson_manifest.course_id,
);
Ok(())
}
#[cfg_attr(coverage, coverage(off))]
fn verify_lesson_manifest(
course_manifest: &CourseManifest,
lesson_manifest: &LessonManifest,
) -> Result<()> {
ensure!(!lesson_manifest.id.is_empty(), "ID in manifest is empty",);
ensure!(
lesson_manifest.course_id == course_manifest.id,
"course_id in manifest for lesson {} does not match the manifest for course {}",
lesson_manifest.id,
course_manifest.id,
);
Ok(())
}
fn process_lesson_manifest(
lesson_root: &Path,
course_manifest: &CourseManifest,
lesson_manifest: LessonManifest,
) -> Result<(LessonManifest, Vec<ExerciseManifest>)> {
LocalCourseLibrary::verify_lesson_manifest(course_manifest, &lesson_manifest)?;
let mut exercises = Vec::new();
for entry in WalkDir::new(lesson_root)
.min_depth(2)
.max_depth(2)
.into_iter()
.flatten()
{
if entry.path().is_dir() {
continue; }
let file_name = Self::get_file_name(entry.path())?;
if file_name != EXERCISE_MANIFEST_FILENAME {
continue;
}
let mut exercise_manifest: ExerciseManifest = Self::open_manifest(entry.path())?;
exercise_manifest =
exercise_manifest.normalize_paths(entry.path().parent().unwrap())?;
LocalCourseLibrary::verify_exercise_manifest(&lesson_manifest, &exercise_manifest)?;
exercises.push(exercise_manifest);
}
Ok((lesson_manifest, exercises))
}
#[cfg_attr(coverage, coverage(off))]
fn verify_course_manifest(course_manifest: &CourseManifest) -> Result<()> {
ensure!(!course_manifest.id.is_empty(), "ID in manifest is empty",);
Ok(())
}
fn process_course_manifest(
&self,
course_root: &Path,
mut course_manifest: CourseManifest,
) -> Result<OpenCourseResult> {
LocalCourseLibrary::verify_course_manifest(&course_manifest)?;
let mut lessons = Vec::new();
if let Some(generator_config) = &course_manifest.generator_config {
let generated_course = generator_config.generate_manifests(
course_root,
&course_manifest,
&self.user_preferences,
)?;
lessons.extend(generated_course.lessons);
if generated_course.updated_metadata.is_some() {
course_manifest.metadata = generated_course.updated_metadata;
}
if generated_course.updated_instructions.is_some() {
course_manifest.course_instructions = generated_course.updated_instructions;
}
}
for entry in WalkDir::new(course_root)
.min_depth(2)
.max_depth(2)
.into_iter()
.flatten()
{
if entry.path().is_dir() {
continue;
}
let file_name = Self::get_file_name(entry.path())?;
if file_name != LESSON_MANIFEST_FILENAME {
continue;
}
let mut lesson_manifest: LessonManifest = Self::open_manifest(entry.path())?;
lesson_manifest = lesson_manifest.normalize_paths(entry.path().parent().unwrap())?;
lessons.push(Self::process_lesson_manifest(
entry.path().parent().unwrap(),
&course_manifest,
lesson_manifest,
)?);
}
Ok(OpenCourseResult {
manifest: course_manifest,
lessons,
})
}
fn process_results(&mut self, courses: Vec<OpenCourseResult>) -> Result<()> {
let mut encompassing_equals_dependency = true;
let mut graph = self.unit_graph.write();
for course in courses {
graph.add_course(course.manifest.id)?;
graph.add_dependencies(
course.manifest.id,
UnitType::Course,
&course.manifest.dependencies,
)?;
graph.add_encompassed(
course.manifest.id,
&course.manifest.dependencies,
&course.manifest.encompassed,
)?;
graph.add_superseded(course.manifest.id, &course.manifest.superseded);
if !course.manifest.encompassed.is_empty() {
encompassing_equals_dependency = false;
}
self.course_map
.insert(course.manifest.id, Arc::new(course.manifest));
for (lesson_manifest, exercises) in course.lessons {
graph.add_lesson(lesson_manifest.id, lesson_manifest.course_id)?;
graph.add_dependencies(
lesson_manifest.id,
UnitType::Lesson,
&lesson_manifest.dependencies,
)?;
graph.add_encompassed(
lesson_manifest.id,
&lesson_manifest.dependencies,
&lesson_manifest.encompassed,
)?;
graph.add_superseded(lesson_manifest.id, &lesson_manifest.superseded);
if !lesson_manifest.encompassed.is_empty() {
encompassing_equals_dependency = false;
}
self.lesson_map
.insert(lesson_manifest.id, Arc::new(lesson_manifest));
for exercise_manifest in exercises {
graph.add_exercise(exercise_manifest.id, exercise_manifest.lesson_id)?;
self.exercise_map
.insert(exercise_manifest.id, Arc::new(exercise_manifest));
}
}
}
graph.update_starting_lessons();
if encompassing_equals_dependency {
graph.set_encompasing_equals_dependency();
}
graph.check_cycles()?;
Ok(())
}
pub fn new(library_root: &Path, user_preferences: UserPreferences) -> Result<Self> {
let mut library = LocalCourseLibrary {
course_map: UstrMap::default(),
lesson_map: UstrMap::default(),
exercise_map: UstrMap::default(),
user_preferences,
unit_graph: Arc::new(RwLock::new(InMemoryUnitGraph::default())),
};
let absolute_root = path::absolute(library_root)?;
let ignored_paths = library
.user_preferences
.ignored_paths
.iter()
.map(|path| {
let mut absolute_path = absolute_root.clone();
absolute_path.push(path);
absolute_path
})
.collect::<Vec<_>>();
let mut courses = Vec::new();
for entry in WalkDir::new(library_root)
.min_depth(2)
.into_iter()
.flatten()
{
if entry.path().is_dir() {
continue;
}
let file_name = Self::get_file_name(entry.path())?;
if file_name != COURSE_MANIFEST_FILENAME {
continue;
}
if ignored_paths
.iter()
.any(|ignored_path| entry.path().starts_with(ignored_path))
{
continue;
}
let mut course_manifest: CourseManifest = Self::open_manifest(entry.path())?;
let parent = entry.path().parent().unwrap();
course_manifest = course_manifest.normalize_paths(parent)?;
courses.push(OpenCourseRequest {
course_root: parent.to_path_buf(),
course_manifest,
});
}
let course_results = courses
.into_par_iter()
.map(|course| {
library.process_course_manifest(&course.course_root, course.course_manifest)
})
.collect::<Result<Vec<_>>>()?;
library.process_results(course_results)?;
Ok(library)
}
pub fn new_from_serialized(
serialized_library: SerializedCourseLibrary,
user_preferences: UserPreferences,
) -> Result<Self> {
Ok(LocalCourseLibrary {
course_map: serialized_library
.course_map
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect(),
lesson_map: serialized_library
.lesson_map
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect(),
exercise_map: serialized_library
.exercise_map
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect(),
user_preferences,
unit_graph: Arc::new(RwLock::new(serialized_library.unit_graph)),
})
}
}
impl CourseLibrary for LocalCourseLibrary {
fn get_course_manifest(&self, course_id: Ustr) -> Option<Arc<CourseManifest>> {
self.course_map.get(&course_id).cloned()
}
fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<Arc<LessonManifest>> {
self.lesson_map.get(&lesson_id).cloned()
}
fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<Arc<ExerciseManifest>> {
self.exercise_map.get(&exercise_id).cloned()
}
fn get_course_ids(&self) -> Vec<Ustr> {
let mut courses = self.course_map.keys().copied().collect::<Vec<Ustr>>();
courses.sort();
courses
}
fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>> {
let mut lessons = self
.unit_graph
.read()
.get_course_lessons(course_id)?
.iter()
.copied()
.collect::<Vec<Ustr>>();
lessons.sort();
Some(lessons)
}
fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>> {
let mut exercises = self
.unit_graph
.read()
.get_lesson_exercises(lesson_id)?
.iter()
.copied()
.collect::<Vec<Ustr>>();
exercises.sort();
Some(exercises)
}
fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr> {
let unit_graph = self.unit_graph.read();
let mut exercises = match unit_id {
Some(unit_id) => {
let unit_type = unit_graph.get_unit_type(unit_id);
match unit_type {
Some(UnitType::Course) => unit_graph
.get_course_lessons(unit_id)
.unwrap_or_default()
.iter()
.copied()
.flat_map(|lesson_id| {
unit_graph
.get_lesson_exercises(lesson_id)
.unwrap_or_default()
.iter()
.copied()
.collect::<Vec<Ustr>>()
})
.collect::<Vec<Ustr>>(),
Some(UnitType::Lesson) => unit_graph
.get_lesson_exercises(unit_id)
.unwrap_or_default()
.iter()
.copied()
.collect::<Vec<Ustr>>(),
Some(UnitType::Exercise) => vec![unit_id],
None => vec![],
}
}
None => self.exercise_map.keys().copied().collect::<Vec<Ustr>>(),
};
exercises.sort();
exercises
}
fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet {
match unit_type {
Some(UnitType::Course) => self
.course_map
.iter()
.filter_map(|(id, _)| {
if id.starts_with(prefix) {
Some(*id)
} else {
None
}
})
.collect(),
Some(UnitType::Lesson) => self
.lesson_map
.iter()
.filter_map(|(id, _)| {
if id.starts_with(prefix) {
Some(*id)
} else {
None
}
})
.collect(),
Some(UnitType::Exercise) => self
.exercise_map
.iter()
.filter_map(|(id, _)| {
if id.starts_with(prefix) {
Some(*id)
} else {
None
}
})
.collect(),
None => self
.course_map
.keys()
.chain(self.lesson_map.keys())
.chain(self.exercise_map.keys())
.filter(|id| id.starts_with(prefix))
.copied()
.collect(),
}
}
}
impl GetUnitGraph for LocalCourseLibrary {
fn get_unit_graph(&self) -> Arc<RwLock<InMemoryUnitGraph>> {
self.unit_graph.clone()
}
}