use anyhow::{Context, Result, ensure};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashSet},
fs::{self, File, create_dir_all},
io::Write,
path::{Path, PathBuf},
};
use ustr::Ustr;
use crate::{
course_builder::AssetBuilder,
course_library::COURSE_MANIFEST_FILENAME,
data::{CourseManifest, course_generator::knowledge_base::*},
};
pub struct ExerciseBuilder {
pub exercise: KnowledgeBaseExercise,
pub asset_builders: Vec<AssetBuilder>,
}
impl ExerciseBuilder {
pub fn build(&self, lesson_directory: &Path) -> Result<()> {
for builder in &self.asset_builders {
builder.build(lesson_directory)?;
}
if let Some(name) = &self.exercise.name {
let name_json = serde_json::to_string_pretty(name)?;
let name_path = lesson_directory.join(format!(
"{}{}",
self.exercise.short_id, EXERCISE_NAME_SUFFIX
));
let mut name_file = File::create(name_path)?;
name_file.write_all(name_json.as_bytes())?;
}
if let Some(description) = &self.exercise.description {
let description_json = serde_json::to_string_pretty(description)?;
let description_path = lesson_directory.join(format!(
"{}{}",
self.exercise.short_id, EXERCISE_DESCRIPTION_SUFFIX
));
let mut description_file = File::create(description_path)?;
description_file.write_all(description_json.as_bytes())?;
}
if let Some(exercise_type) = &self.exercise.exercise_type {
let exercise_type_json = serde_json::to_string_pretty(exercise_type)?;
let exercise_type_path = lesson_directory.join(format!(
"{}{}",
self.exercise.short_id, EXERCISE_TYPE_SUFFIX
));
let mut exercise_type_file = File::create(exercise_type_path)?;
exercise_type_file.write_all(exercise_type_json.as_bytes())?;
}
Ok(())
}
}
pub struct LessonBuilder {
pub lesson: KnowledgeBaseLesson,
pub exercises: Vec<ExerciseBuilder>,
pub asset_builders: Vec<AssetBuilder>,
}
impl LessonBuilder {
pub fn build(&self, lesson_directory: &Path) -> Result<()> {
create_dir_all(lesson_directory)?;
for builder in &self.asset_builders {
builder.build(lesson_directory)?;
}
for builder in &self.exercises {
builder.build(lesson_directory)?;
}
if let Some(name) = &self.lesson.name {
let name_json = serde_json::to_string_pretty(name)?;
let name_path = lesson_directory.join(LESSON_NAME_FILE);
let mut name_file = File::create(name_path)?;
name_file.write_all(name_json.as_bytes())?;
}
if let Some(description) = &self.lesson.description {
let description_json = serde_json::to_string_pretty(description)?;
let description_path = lesson_directory.join(LESSON_DESCRIPTION_FILE);
let mut description_file = File::create(description_path)?;
description_file.write_all(description_json.as_bytes())?;
}
if !self.lesson.dependencies.is_empty() {
let dependencies_json = serde_json::to_string_pretty(&self.lesson.dependencies)?;
let dependencies_path = lesson_directory.join(LESSON_DEPENDENCIES_FILE);
let mut dependencies_file = File::create(dependencies_path)?;
dependencies_file.write_all(dependencies_json.as_bytes())?;
}
if !self.lesson.superseded.is_empty() {
let superseded_json = serde_json::to_string_pretty(&self.lesson.superseded)?;
let superseded_path = lesson_directory.join(LESSON_SUPERSEDED_FILE);
let mut superseded_file = File::create(superseded_path)?;
superseded_file.write_all(superseded_json.as_bytes())?;
}
if !self.lesson.encompassed.is_empty() {
let encompassed_json = serde_json::to_string_pretty(&self.lesson.encompassed)?;
let encompassed_path = lesson_directory.join(LESSON_ENCOMPASSED_FILE);
let mut encompassed_file = File::create(encompassed_path)?;
encompassed_file.write_all(encompassed_json.as_bytes())?;
}
if let Some(metadata) = &self.lesson.metadata {
let metadata_json = serde_json::to_string_pretty(metadata)?;
let metadata_path = lesson_directory.join(LESSON_METADATA_FILE);
let mut metadata_file = File::create(metadata_path)?;
metadata_file.write_all(metadata_json.as_bytes())?;
}
if let Some(default_exercise_type) = &self.lesson.default_exercise_type {
let type_json = serde_json::to_string_pretty(default_exercise_type)?;
let type_path = lesson_directory.join(LESSON_DEFAULT_EXERCISE_TYPE_FILE);
let mut type_file = File::create(type_path)?;
type_file.write_all(type_json.as_bytes())?;
}
Ok(())
}
}
pub struct CourseBuilder {
pub directory_name: String,
pub lessons: Vec<LessonBuilder>,
pub assets: Vec<AssetBuilder>,
pub manifest: CourseManifest,
}
impl CourseBuilder {
#[cfg_attr(coverage, coverage(off))]
fn create_course_directory(&self, parent_directory: &Path) -> Result<PathBuf> {
let course_directory = parent_directory.join(&self.directory_name);
ensure!(
!course_directory.is_dir(),
"course directory {} already exists",
course_directory.display(),
);
create_dir_all(&course_directory)?;
Ok(course_directory)
}
pub fn build(&self, parent_directory: &Path) -> Result<()> {
let course_directory = self.create_course_directory(parent_directory)?;
for builder in &self.assets {
builder.build(&course_directory)?;
}
for builder in &self.lessons {
let lesson_directory =
course_directory.join(format!("{}{}", builder.lesson.short_id, LESSON_SUFFIX));
fs::create_dir_all(&lesson_directory)?;
builder.build(&lesson_directory)?;
}
let manifest_json = serde_json::to_string_pretty(&self.manifest)? + "\n";
let manifest_path = course_directory.join("course_manifest.json");
let mut manifest_file = File::create(manifest_path)?;
manifest_file.write_all(manifest_json.as_bytes())?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseExercise {
pub short_id: String,
pub front: Vec<String>,
#[serde(default)]
pub back: Vec<String>,
}
impl SimpleKnowledgeBaseExercise {
fn generate_exercise_builder(
&self,
short_lesson_id: Ustr,
course_id: Ustr,
) -> Result<ExerciseBuilder> {
ensure!(!self.short_id.is_empty(), "short ID cannot be empty");
let front_file = format!("{}{}", self.short_id, EXERCISE_FRONT_SUFFIX);
let back_file = if self.back.is_empty() {
None
} else {
Some(format!("{}{}", self.short_id, EXERCISE_BACK_SUFFIX))
};
let mut asset_builders = vec![AssetBuilder {
file_name: front_file.clone(),
contents: self.front.join("\n"),
}];
if !self.back.is_empty() {
asset_builders.push(AssetBuilder {
file_name: back_file.clone().unwrap(),
contents: self.back.join("\n"),
});
}
Ok(ExerciseBuilder {
exercise: KnowledgeBaseExercise {
short_id: self.short_id.clone(),
short_lesson_id,
course_id,
front_file,
back_file,
name: None,
description: None,
exercise_type: None,
},
asset_builders,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseLesson {
pub short_id: Ustr,
#[serde(default)]
pub dependencies: Vec<Ustr>,
#[serde(default)]
pub superseded: Vec<Ustr>,
#[serde(default)]
pub encompassed: Vec<(Ustr, f32)>,
#[serde(default)]
pub exercises: Vec<SimpleKnowledgeBaseExercise>,
#[serde(default)]
pub metadata: Option<BTreeMap<String, Vec<String>>>,
#[serde(default)]
pub additional_files: Vec<AssetBuilder>,
}
impl SimpleKnowledgeBaseLesson {
fn generate_lesson_builder(&self, course_id: Ustr) -> Result<LessonBuilder> {
ensure!(
!self.short_id.is_empty(),
"short ID of lesson cannot be empty"
);
let mut short_ids = HashSet::new();
for exercise in &self.exercises {
ensure!(
!short_ids.contains(&exercise.short_id),
"short ID {} of exercise is not unique",
exercise.short_id
);
short_ids.insert(&exercise.short_id);
}
let exercises = self
.exercises
.iter()
.map(|exercise| exercise.generate_exercise_builder(self.short_id, course_id))
.collect::<Result<Vec<_>>>()?;
let has_instructions = self
.additional_files
.iter()
.any(|asset| asset.file_name == LESSON_INSTRUCTIONS_FILE);
let has_material = self
.additional_files
.iter()
.any(|asset| asset.file_name == LESSON_MATERIAL_FILE);
let lesson_builder = LessonBuilder {
lesson: KnowledgeBaseLesson {
short_id: self.short_id,
course_id,
dependencies: self.dependencies.clone(),
encompassed: self.encompassed.clone(),
superseded: self.superseded.clone(),
name: None,
description: None,
metadata: self.metadata.clone(),
has_instructions,
has_material,
default_exercise_type: None,
},
exercises,
asset_builders: self.additional_files.clone(),
};
Ok(lesson_builder)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseCourse {
pub manifest: CourseManifest,
#[serde(default)]
pub encompassed: Vec<(Ustr, f32)>,
#[serde(default)]
pub lessons: Vec<SimpleKnowledgeBaseLesson>,
}
impl SimpleKnowledgeBaseCourse {
pub fn build(&self, root_directory: &Path) -> Result<()> {
let mut short_ids = HashSet::new();
for lesson in &self.lessons {
ensure!(
!short_ids.contains(&lesson.short_id),
"short ID {} of lesson is not unique",
lesson.short_id
);
short_ids.insert(&lesson.short_id);
}
let lesson_builders = self
.lessons
.iter()
.map(|lesson| lesson.generate_lesson_builder(self.manifest.id))
.collect::<Result<Vec<_>>>()?;
let mut manifest = self.manifest.clone();
if !self.encompassed.is_empty() {
manifest.encompassed.clone_from(&self.encompassed);
}
for lesson_builder in lesson_builders {
let lesson_directory = root_directory.join(format!(
"{}{}",
lesson_builder.lesson.short_id, LESSON_SUFFIX
));
if lesson_directory.exists() {
fs::remove_dir_all(&lesson_directory).context(format!(
"failed to remove existing lesson directory at {}",
lesson_directory.display()
))?;
}
lesson_builder.build(&lesson_directory)?;
}
let manifest_path = root_directory.join(COURSE_MANIFEST_FILENAME);
let display = manifest_path.display();
let mut manifest_file = fs::File::create(&manifest_path).context(format!(
"failed to create course manifest file at {display}"
))?;
manifest_file
.write_all(serde_json::to_string_pretty(&manifest)?.as_bytes())
.context(format!("failed to write course manifest file at {display}"))
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use std::collections::BTreeMap;
use anyhow::Result;
use crate::{
course_builder::knowledge_base_builder::*,
data::{ExerciseType, course_generator::knowledge_base::KnowledgeBaseFile},
};
fn test_lesson_builder() -> LessonBuilder {
let exercise_builder = ExerciseBuilder {
exercise: KnowledgeBaseExercise {
short_id: "ex1".to_string(),
short_lesson_id: "lesson1".into(),
course_id: "course1".into(),
front_file: "ex1.front.md".to_string(),
back_file: Some("ex1.back.md".to_string()),
name: Some("Exercise 1".to_string()),
description: Some("Exercise 1 description".to_string()),
exercise_type: Some(ExerciseType::Procedural),
},
asset_builders: vec![
AssetBuilder {
file_name: "ex1.front.md".to_string(),
contents: "Exercise 1 front".to_string(),
},
AssetBuilder {
file_name: "ex1.back.md".to_string(),
contents: "Exercise 1 back".to_string(),
},
],
};
LessonBuilder {
lesson: KnowledgeBaseLesson {
short_id: "lesson1".into(),
course_id: "course1".into(),
name: Some("Lesson 1".to_string()),
description: Some("Lesson 1 description".to_string()),
dependencies: vec!["lesson2".into()],
encompassed: vec![("lesson3".into(), 0.5)],
superseded: vec!["lesson0".into()],
metadata: Some(BTreeMap::from([(
"key".to_string(),
vec!["value".to_string()],
)])),
has_instructions: true,
has_material: true,
default_exercise_type: Some(ExerciseType::Declarative),
},
exercises: vec![exercise_builder],
asset_builders: vec![
AssetBuilder {
file_name: LESSON_INSTRUCTIONS_FILE.to_string(),
contents: "Instructions".to_string(),
},
AssetBuilder {
file_name: LESSON_MATERIAL_FILE.to_string(),
contents: "Material".to_string(),
},
],
}
}
#[test]
fn course_builder() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let course_builder = CourseBuilder {
directory_name: "course1".into(),
manifest: CourseManifest {
id: "course1".into(),
name: "Course 1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
},
lessons: vec![test_lesson_builder()],
assets: vec![
AssetBuilder {
file_name: "course_instructions.md".to_string(),
contents: "Course Instructions".to_string(),
},
AssetBuilder {
file_name: "course_material.md".to_string(),
contents: "Course Material".to_string(),
},
],
};
course_builder.build(temp_dir.path())?;
let course_dir = temp_dir.path().join("course1");
let lesson_dir = course_dir.join("lesson1.lesson");
assert!(lesson_dir.exists());
let front_file = lesson_dir.join("ex1.front.md");
assert!(front_file.exists());
assert_eq!(fs::read_to_string(front_file)?, "Exercise 1 front");
let back_file = lesson_dir.join("ex1.back.md");
assert!(back_file.exists());
assert_eq!(fs::read_to_string(back_file)?, "Exercise 1 back");
let name_file = lesson_dir.join("ex1.name.json");
assert!(name_file.exists());
assert_eq!(KnowledgeBaseFile::open::<String>(&name_file)?, "Exercise 1",);
let description_file = lesson_dir.join("ex1.description.json");
assert!(description_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<String>(&description_file)?,
"Exercise 1 description",
);
let type_file = lesson_dir.join("ex1.type.json");
assert!(type_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<ExerciseType>(&type_file)?,
ExerciseType::Procedural,
);
let name_file = lesson_dir.join(LESSON_NAME_FILE);
assert!(name_file.exists());
assert_eq!(KnowledgeBaseFile::open::<String>(&name_file)?, "Lesson 1",);
let description_file = lesson_dir.join(LESSON_DESCRIPTION_FILE);
assert!(description_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<String>(&description_file)?,
"Lesson 1 description",
);
let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
assert!(lesson_dir.join(LESSON_DEPENDENCIES_FILE).exists());
assert_eq!(
KnowledgeBaseFile::open::<Vec<String>>(&dependencies_file)?,
vec!["lesson2".to_string()],
);
let metadata_file = lesson_dir.join(LESSON_METADATA_FILE);
assert!(metadata_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<BTreeMap<String, Vec<String>>>(&metadata_file)?,
BTreeMap::from([("key".to_string(), vec!["value".to_string()])]),
);
let encompassed_file = lesson_dir.join(LESSON_ENCOMPASSED_FILE);
assert!(encompassed_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<Vec<(String, f32)>>(&encompassed_file)?,
vec![("lesson3".to_string(), 0.5)],
);
let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
assert!(instructions_file.exists());
assert_eq!(fs::read_to_string(instructions_file)?, "Instructions",);
let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
assert!(material_file.exists());
assert_eq!(fs::read_to_string(material_file)?, "Material",);
assert!(course_dir.join("course_manifest.json").exists());
assert_eq!(
KnowledgeBaseFile::open::<CourseManifest>(&course_dir.join("course_manifest.json"))
.unwrap(),
course_builder.manifest,
);
assert!(course_dir.join("course_instructions.md").exists());
assert_eq!(
fs::read_to_string(course_dir.join("course_instructions.md"))?,
"Course Instructions",
);
assert!(course_dir.join("course_material.md").exists());
assert_eq!(
fs::read_to_string(course_dir.join("course_material.md"))?,
"Course Material",
);
Ok(())
}
#[test]
fn build_simple_course() -> Result<()> {
let simple_course = SimpleKnowledgeBaseCourse {
manifest: CourseManifest {
id: "course1".into(),
name: "Course 1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
},
encompassed: vec![("course2".into(), 0.5)],
lessons: vec![
SimpleKnowledgeBaseLesson {
short_id: "1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
exercises: vec![
SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 1, Exercise 1 front".into()],
back: vec![],
},
SimpleKnowledgeBaseExercise {
short_id: "2".into(),
front: vec!["Lesson 1, Exercise 2 front".into()],
back: vec![],
},
],
metadata: None,
additional_files: vec![],
},
SimpleKnowledgeBaseLesson {
short_id: "2".into(),
dependencies: vec!["1".into()],
encompassed: vec![("lesson1".into(), 0.25)],
superseded: vec!["0".into()],
exercises: vec![
SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 2, Exercise 1 front".into()],
back: vec!["Lesson 2, Exercise 1 back".into()],
},
SimpleKnowledgeBaseExercise {
short_id: "2".into(),
front: vec!["Lesson 2, Exercise 2 front".into()],
back: vec!["Lesson 2, Exercise 2 back".into()],
},
],
metadata: Some(BTreeMap::from([(
"key".to_string(),
vec!["value".to_string()],
)])),
additional_files: vec![
AssetBuilder {
file_name: "dummy.md".into(),
contents: "I'm a dummy file".into(),
},
AssetBuilder {
file_name: LESSON_INSTRUCTIONS_FILE.into(),
contents: "Lesson 2 instructions".into(),
},
AssetBuilder {
file_name: LESSON_MATERIAL_FILE.into(),
contents: "Lesson 2 material".into(),
},
],
},
],
};
let temp_dir = tempfile::tempdir()?;
let dummy_dir = temp_dir.path().join("1.lesson").join("dummy");
fs::create_dir_all(&dummy_dir)?;
assert!(dummy_dir.exists());
simple_course.build(temp_dir.path())?;
assert!(!dummy_dir.exists());
let lesson_dir = temp_dir.path().join("1.lesson");
assert!(lesson_dir.exists());
let front_file = lesson_dir.join("1.front.md");
assert!(front_file.exists());
assert_eq!(
fs::read_to_string(&front_file)?,
"Lesson 1, Exercise 1 front"
);
let front_file = lesson_dir.join("2.front.md");
assert!(front_file.exists());
assert_eq!(
fs::read_to_string(&front_file)?,
"Lesson 1, Exercise 2 front"
);
let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
assert!(!dependencies_file.exists());
let superseced_file = lesson_dir.join(LESSON_SUPERSEDED_FILE);
assert!(!superseced_file.exists());
let encompassed_file = lesson_dir.join(LESSON_ENCOMPASSED_FILE);
assert!(!encompassed_file.exists());
let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
assert!(!instructions_file.exists());
let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
assert!(!material_file.exists());
let lesson_dir = temp_dir.path().join("2.lesson");
assert!(lesson_dir.exists());
let front_file = lesson_dir.join("1.front.md");
assert!(front_file.exists());
assert_eq!(
fs::read_to_string(&front_file)?,
"Lesson 2, Exercise 1 front"
);
let back_file = lesson_dir.join("1.back.md");
assert!(back_file.exists());
assert_eq!(fs::read_to_string(&back_file)?, "Lesson 2, Exercise 1 back");
let front_file = lesson_dir.join("2.front.md");
assert!(front_file.exists());
assert_eq!(
fs::read_to_string(&front_file)?,
"Lesson 2, Exercise 2 front"
);
let back_file = lesson_dir.join("2.back.md");
assert!(back_file.exists());
assert_eq!(fs::read_to_string(&back_file)?, "Lesson 2, Exercise 2 back");
let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
assert!(dependencies_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<Vec<String>>(&dependencies_file)?,
vec!["1".to_string()]
);
let superseced_file = lesson_dir.join(LESSON_SUPERSEDED_FILE);
assert!(superseced_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<Vec<String>>(&superseced_file)?,
vec!["0".to_string()]
);
let encompassed_file = lesson_dir.join(LESSON_ENCOMPASSED_FILE);
assert!(encompassed_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<Vec<(String, f32)>>(&encompassed_file)?,
vec![("lesson1".to_string(), 0.25)]
);
let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
assert!(instructions_file.exists());
assert_eq!(
fs::read_to_string(&instructions_file)?,
"Lesson 2 instructions"
);
let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
assert!(material_file.exists());
assert_eq!(fs::read_to_string(&material_file)?, "Lesson 2 material");
let metadata_file = lesson_dir.join(LESSON_METADATA_FILE);
assert!(metadata_file.exists());
assert_eq!(
KnowledgeBaseFile::open::<BTreeMap<String, Vec<String>>>(&metadata_file)?,
BTreeMap::from([("key".to_string(), vec!["value".to_string()])])
);
let dummy_file = lesson_dir.join("dummy.md");
assert!(dummy_file.exists());
assert_eq!(fs::read_to_string(&dummy_file)?, "I'm a dummy file");
assert_eq!(
KnowledgeBaseFile::open::<CourseManifest>(
&temp_dir.path().join("course_manifest.json")
)?
.encompassed,
vec![("course2".into(), 0.5)],
);
assert_eq!(simple_course.clone(), simple_course);
Ok(())
}
#[test]
fn duplicate_short_lesson_ids() -> Result<()> {
let simple_course = SimpleKnowledgeBaseCourse {
manifest: CourseManifest {
id: "course1".into(),
name: "Course 1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
},
encompassed: vec![],
lessons: vec![
SimpleKnowledgeBaseLesson {
short_id: "1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
exercises: vec![SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 1, Exercise 1 front".into()],
back: vec![],
}],
metadata: None,
additional_files: vec![],
},
SimpleKnowledgeBaseLesson {
short_id: "1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
exercises: vec![SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 2, Exercise 1 front".into()],
back: vec![],
}],
metadata: None,
additional_files: vec![],
},
],
};
let temp_dir = tempfile::tempdir()?;
assert!(simple_course.build(temp_dir.path()).is_err());
Ok(())
}
#[test]
fn duplicate_short_exercise_ids() -> Result<()> {
let simple_course = SimpleKnowledgeBaseCourse {
manifest: CourseManifest {
id: "course1".into(),
name: "Course 1".into(),
dependencies: vec![],
superseded: vec![],
encompassed: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
},
encompassed: vec![],
lessons: vec![SimpleKnowledgeBaseLesson {
short_id: "1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
exercises: vec![
SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 1, Exercise 1 front".into()],
back: vec![],
},
SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 1, Exercise 2 front".into()],
back: vec![],
},
],
metadata: None,
additional_files: vec![],
}],
};
let temp_dir = tempfile::tempdir()?;
assert!(simple_course.build(temp_dir.path()).is_err());
Ok(())
}
#[test]
fn empty_short_lesson_ids() -> Result<()> {
let simple_course = SimpleKnowledgeBaseCourse {
manifest: CourseManifest {
id: "course1".into(),
name: "Course 1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
},
encompassed: vec![],
lessons: vec![SimpleKnowledgeBaseLesson {
short_id: "".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
exercises: vec![SimpleKnowledgeBaseExercise {
short_id: "1".into(),
front: vec!["Lesson 1, Exercise 1 front".into()],
back: vec![],
}],
metadata: None,
additional_files: vec![],
}],
};
let temp_dir = tempfile::tempdir()?;
assert!(simple_course.build(temp_dir.path()).is_err());
Ok(())
}
#[test]
fn empty_short_exercise_ids() -> Result<()> {
let simple_course = SimpleKnowledgeBaseCourse {
manifest: CourseManifest {
id: "course1".into(),
name: "Course 1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
},
encompassed: vec![],
lessons: vec![SimpleKnowledgeBaseLesson {
short_id: "1".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
exercises: vec![SimpleKnowledgeBaseExercise {
short_id: String::new(),
front: vec!["Lesson 1, Exercise 1 front".into()],
back: vec![],
}],
metadata: None,
additional_files: vec![],
}],
};
let temp_dir = tempfile::tempdir()?;
assert!(simple_course.build(temp_dir.path()).is_err());
Ok(())
}
}