use anyhow::{Context, Error, Result, anyhow};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{
collections::BTreeMap,
fs::{File, read_dir},
io::{BufReader, Read},
path::Path,
};
use strum::Display;
use ustr::{Ustr, UstrMap, UstrSet};
use crate::data::{
BasicAsset, CourseGenerator, CourseManifest, ExerciseAsset, ExerciseManifest, ExerciseType,
GenerateManifests, GeneratedCourse, LessonManifest, UserPreferences,
};
pub const COURSE_METADATA: &str = "literacy_course";
pub const COURSE_INSTRUCTIONS_FILE: &str = "course.instructions.md";
pub const LESSON_SUFFIX: &str = ".lesson";
pub const LESSON_DEPENDENCIES_FILE: &str = "lesson.dependencies.json";
pub const LESSON_NAME_FILE: &str = "lesson.name.json";
pub const LESSON_DESCRIPTION_FILE: &str = "lesson.description.json";
pub const LESSON_INSTRUCTIONS_FILE: &str = "lesson.instructions.md";
pub const LESSON_MATERIAL_FILE: &str = "lesson.material.md";
pub const LESSON_METADATA: &str = "literacy_lesson";
pub const EXAMPLE_SUFFIX: &str = ".example.md";
pub const EXCEPTION_SUFFIX: &str = ".exception.md";
pub const SIMPLE_EXAMPLES_FILE: &str = "simple_examples.md";
pub const SIMPLE_EXCEPTIONS_FILE: &str = "simple_exceptions.md";
#[derive(Debug, Eq, PartialEq)]
pub enum LiteracyFile {
CourseInstructions,
LessonName,
LessonDescription,
LessonDependencies,
LessonInstructions,
Example(String),
Exception(String),
SimpleExamples,
SimpleExceptions,
}
impl LiteracyFile {
pub fn open_serialized<T: DeserializeOwned>(path: &Path) -> Result<T> {
let display = path.display();
let file = File::open(path).context(format!("cannot open literacy file {display}"))?;
let reader = BufReader::new(file);
serde_json::from_reader(reader).context(format!("cannot parse literacy file {display}"))
}
pub fn open_md(path: &Path) -> Result<String> {
let display = path.display();
let file =
File::open(path).context(format!("cannot open literacy markdown file {display}"))?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
reader
.read_to_string(&mut contents)
.context(format!("cannot read literacy markdown file {display}"))?;
Ok(contents)
}
pub fn open_md_list(path: &Path) -> Result<Vec<String>> {
let display = path.display();
let file =
File::open(path).context(format!("cannot open literacy markdown file {display}"))?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
reader
.read_to_string(&mut contents)
.context(format!("cannot read literacy markdown file {display}"))?;
Ok(contents
.lines()
.map(ToString::to_string)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect())
}
}
impl TryFrom<&str> for LiteracyFile {
type Error = Error;
fn try_from(file_name: &str) -> Result<Self> {
match file_name {
LESSON_DEPENDENCIES_FILE => Ok(LiteracyFile::LessonDependencies),
LESSON_NAME_FILE => Ok(LiteracyFile::LessonName),
LESSON_DESCRIPTION_FILE => Ok(LiteracyFile::LessonDescription),
LESSON_INSTRUCTIONS_FILE => Ok(LiteracyFile::LessonInstructions),
file_name if file_name.ends_with(EXAMPLE_SUFFIX) => {
let short_id = file_name.strip_suffix(EXAMPLE_SUFFIX).unwrap();
Ok(LiteracyFile::Example(short_id.to_string()))
}
file_name if file_name.ends_with(EXCEPTION_SUFFIX) => {
let short_id = file_name.strip_suffix(EXCEPTION_SUFFIX).unwrap();
Ok(LiteracyFile::Exception(short_id.to_string()))
}
SIMPLE_EXAMPLES_FILE => Ok(LiteracyFile::SimpleExamples),
SIMPLE_EXCEPTIONS_FILE => Ok(LiteracyFile::SimpleExceptions),
_ => Err(anyhow!("Not a valid literacy file name: {file_name}")), }
}
}
#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)]
pub enum LiteracyLessonType {
Reading,
Dictation,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LiteracyLesson {
pub short_id: Ustr,
pub dependencies: Vec<Ustr>,
pub name: Option<String>,
pub description: Option<String>,
pub instructions: Option<BasicAsset>,
pub examples: Vec<String>,
pub exceptions: Vec<String>,
}
impl LiteracyLesson {
fn create_lesson(
lesson_root: &Path,
short_lesson_id: Ustr,
files: &[LiteracyFile],
) -> Result<Self> {
let mut lesson = Self {
short_id: short_lesson_id,
dependencies: vec![],
name: None,
description: None,
instructions: None,
examples: vec![],
exceptions: vec![],
};
for lesson_file in files {
match lesson_file {
LiteracyFile::CourseInstructions => {
return Err(anyhow!(
"Found course instructions file in lesson directory: {}",
lesson_root.display()
));
}
LiteracyFile::LessonDependencies => {
let path = lesson_root.join(LESSON_DEPENDENCIES_FILE);
lesson.dependencies = LiteracyFile::open_serialized(&path)?;
}
LiteracyFile::LessonName => {
let path = lesson_root.join(LESSON_NAME_FILE);
lesson.name = Some(LiteracyFile::open_serialized(&path)?);
}
LiteracyFile::LessonDescription => {
let path = lesson_root.join(LESSON_DESCRIPTION_FILE);
lesson.description = Some(LiteracyFile::open_serialized(&path)?);
}
LiteracyFile::LessonInstructions => {
let path = lesson_root.join(LESSON_INSTRUCTIONS_FILE);
lesson.instructions = Some(BasicAsset::InlinedAsset {
content: LiteracyFile::open_md(&path)?,
});
}
LiteracyFile::Example(short_id) => {
let path = lesson_root.join(format!("{short_id}{EXAMPLE_SUFFIX}"));
let example = LiteracyFile::open_md(&path)?;
lesson.examples.push(example);
}
LiteracyFile::Exception(short_id) => {
let path = lesson_root.join(format!("{short_id}{EXCEPTION_SUFFIX}"));
let exception = LiteracyFile::open_md(&path)?;
lesson.exceptions.push(exception);
}
LiteracyFile::SimpleExamples => {
let path = lesson_root.join(SIMPLE_EXAMPLES_FILE);
let examples = LiteracyFile::open_md_list(&path)?;
lesson.examples.extend(examples);
}
LiteracyFile::SimpleExceptions => {
let path = lesson_root.join(SIMPLE_EXCEPTIONS_FILE);
let exceptions = LiteracyFile::open_md_list(&path)?;
lesson.exceptions.extend(exceptions);
}
}
}
lesson.examples.sort();
lesson.exceptions.sort();
Ok(lesson)
}
fn open_lesson(lesson_root: &Path, short_lesson_id: Ustr) -> Result<Self> {
let lesson_files = read_dir(lesson_root)?
.flatten()
.flat_map(|entry| {
LiteracyFile::try_from(entry.file_name().to_str().unwrap_or_default())
})
.collect::<Vec<_>>();
Self::create_lesson(lesson_root, short_lesson_id, &lesson_files)
}
fn full_reading_lesson_id(course_id: Ustr, lesson_id: Ustr, short_ids: &UstrSet) -> Ustr {
if short_ids.contains(&lesson_id) {
let full_id = format!("{course_id}::{lesson_id}::reading");
full_id.into()
} else {
lesson_id
}
}
fn full_dictation_lesson_id(course_id: Ustr, lesson_id: Ustr, short_ids: &UstrSet) -> Ustr {
if short_ids.contains(&lesson_id) {
let full_id = format!("{course_id}::{lesson_id}::dictation");
full_id.into()
} else {
lesson_id
}
}
fn course_name(course_manifest: &CourseManifest) -> String {
if course_manifest.name.is_empty() {
course_manifest.id.to_string()
} else {
course_manifest.name.clone()
}
}
fn lesson_name(&self, course_name: &str, lesson_type: &LiteracyLessonType) -> String {
let lesson_type = match lesson_type {
LiteracyLessonType::Reading => "Reading",
LiteracyLessonType::Dictation => "Dictation",
};
if let Some(name) = &self.name {
format!("{course_name} - {name} - {lesson_type}")
} else {
format!("{course_name} - {} - {lesson_type}", self.short_id)
}
}
fn generate_reading_lesson(
&self,
course_manifest: &CourseManifest,
short_id: Ustr,
short_ids: &UstrSet,
) -> (LessonManifest, Vec<ExerciseManifest>) {
let lesson_id = Self::full_reading_lesson_id(course_manifest.id, short_id, short_ids);
let course_name = Self::course_name(course_manifest);
let lesson_name = self.lesson_name(&course_name, &LiteracyLessonType::Reading);
let mut dependencies = self
.dependencies
.iter()
.map(|id| Self::full_reading_lesson_id(course_manifest.id, *id, short_ids))
.collect::<Vec<_>>();
dependencies.sort();
let lesson_manifest = LessonManifest {
id: lesson_id,
dependencies,
encompassed: vec![],
superseded: vec![],
course_id: course_manifest.id,
name: lesson_name.clone(),
description: self.description.clone(),
metadata: Some(BTreeMap::from([(
LESSON_METADATA.to_string(),
vec!["reading".to_string()],
)])),
lesson_instructions: self.instructions.clone(),
lesson_material: None,
};
let exercise_manifest = ExerciseManifest {
id: format!("{lesson_id}::exercise").into(),
lesson_id: lesson_manifest.id,
course_id: course_manifest.id,
name: lesson_name,
description: self.description.clone(),
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Reading,
examples: self.examples.clone(),
exceptions: self.exceptions.clone(),
},
};
(lesson_manifest, vec![exercise_manifest])
}
fn generate_dictation_lesson(
&self,
course_manifest: &CourseManifest,
short_id: Ustr,
short_ids: &UstrSet,
) -> (LessonManifest, Vec<ExerciseManifest>) {
let lesson_id = Self::full_dictation_lesson_id(course_manifest.id, short_id, short_ids);
let course_name = Self::course_name(course_manifest);
let lesson_name = self.lesson_name(&course_name, &LiteracyLessonType::Dictation);
let reading_lesson_id =
Self::full_reading_lesson_id(course_manifest.id, short_id, short_ids);
let mut dependencies = self
.dependencies
.iter()
.filter_map(|id| {
let full_dependency =
Self::full_dictation_lesson_id(course_manifest.id, *id, short_ids);
if full_dependency == *id {
None
} else {
Some(full_dependency)
}
})
.collect::<Vec<_>>();
dependencies.push(reading_lesson_id);
dependencies.sort();
let lesson_manifest = LessonManifest {
id: lesson_id,
dependencies,
encompassed: vec![],
superseded: vec![],
course_id: course_manifest.id,
name: lesson_name.clone(),
description: self.description.clone(),
metadata: Some(BTreeMap::from([(
LESSON_METADATA.to_string(),
vec!["dictation".to_string()],
)])),
lesson_instructions: self.instructions.clone(),
lesson_material: None,
};
let exercise_manifest = ExerciseManifest {
id: format!("{lesson_id}::exercise").into(),
lesson_id: lesson_manifest.id,
course_id: course_manifest.id,
name: lesson_name,
description: self.description.clone(),
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Dictation,
examples: self.examples.clone(),
exceptions: self.exceptions.clone(),
},
};
(lesson_manifest, vec![exercise_manifest])
}
fn generate_manifests(
&self,
course_manifest: &CourseManifest,
short_id: Ustr,
short_ids: &UstrSet,
) -> Vec<(LessonManifest, Vec<ExerciseManifest>)> {
let mut generate_dictation = false;
if let Some(CourseGenerator::Literacy(config)) = &course_manifest.generator_config {
generate_dictation = config.generate_dictation;
}
if generate_dictation {
vec![
self.generate_reading_lesson(course_manifest, short_id, short_ids),
self.generate_dictation_lesson(course_manifest, short_id, short_ids),
]
} else {
vec![self.generate_reading_lesson(course_manifest, short_id, short_ids)]
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct LiteracyConfig {
#[serde(default)]
pub generate_dictation: bool,
}
impl LiteracyConfig {
fn open_course_instructions(course_root: &Path) -> Result<Option<BasicAsset>> {
let path = course_root.join(COURSE_INSTRUCTIONS_FILE);
if path.exists() && path.is_file() {
Ok(Some(BasicAsset::InlinedAsset {
content: LiteracyFile::open_md(&path)?,
}))
} else {
Ok(None) }
}
}
impl GenerateManifests for LiteracyConfig {
fn generate_manifests(
&self,
course_root: &Path,
course_manifest: &CourseManifest,
_preferences: &UserPreferences,
) -> Result<GeneratedCourse> {
let mut lessons = UstrMap::default();
let valid_entries = read_dir(course_root)?
.flatten()
.filter(|entry| {
let path = entry.path();
path.is_dir()
})
.collect::<Vec<_>>();
for entry in valid_entries {
let path = entry.path();
let dir_name = path.file_name().unwrap_or_default().to_str().unwrap();
if let Some(short_id) = dir_name.strip_suffix(LESSON_SUFFIX)
&& !short_id.is_empty()
{
lessons.insert(
short_id.into(),
LiteracyLesson::open_lesson(&path, short_id.into())?,
);
}
}
let short_ids: UstrSet = lessons.keys().copied().collect();
let lessons: Vec<(LessonManifest, Vec<ExerciseManifest>)> = lessons
.into_iter()
.flat_map(|(short_id, lesson)| {
lesson.generate_manifests(course_manifest, short_id, &short_ids)
})
.collect();
let mut metadata = course_manifest.metadata.clone().unwrap_or_default();
metadata.insert(COURSE_METADATA.to_string(), vec!["true".to_string()]);
Ok(GeneratedCourse {
lessons,
updated_metadata: Some(metadata),
updated_instructions: Self::open_course_instructions(course_root)?,
})
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::Result;
use pretty_assertions::assert_eq;
use std::{collections::BTreeMap, fs, path::Path};
use ustr::{Ustr, UstrSet};
use crate::data::{
BasicAsset, CourseGenerator, CourseManifest, ExerciseAsset, ExerciseManifest, ExerciseType,
GenerateManifests, GeneratedCourse, LessonManifest, UserPreferences,
course_generator::literacy::{LiteracyConfig, LiteracyLesson, LiteracyLessonType},
};
#[test]
fn full_lesson_ids() {
let course_id = Ustr::from("course_id");
let short_id = Ustr::from("lesson_id");
let not_in_short_ids = "other_course_id::other_lesson_id".into();
let short_ids: UstrSet = vec!["lesson_id".into()].into_iter().collect();
let reading_lesson_id =
LiteracyLesson::full_reading_lesson_id(course_id, short_id, &short_ids);
assert_eq!(
reading_lesson_id,
Ustr::from("course_id::lesson_id::reading"),
);
let reading_lesson_id =
LiteracyLesson::full_reading_lesson_id(course_id, not_in_short_ids, &short_ids);
assert_eq!(
reading_lesson_id,
Ustr::from("other_course_id::other_lesson_id")
);
let dictation_lesson_id =
LiteracyLesson::full_dictation_lesson_id(course_id, short_id, &short_ids);
assert_eq!(
dictation_lesson_id,
Ustr::from("course_id::lesson_id::dictation"),
);
let dictation_lesson_id =
LiteracyLesson::full_dictation_lesson_id(course_id, not_in_short_ids, &short_ids);
assert_eq!(
dictation_lesson_id,
Ustr::from("other_course_id::other_lesson_id")
);
}
#[test]
fn course_name() {
let course_manifest = CourseManifest {
id: "course_id".into(),
name: "Course Name".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
};
assert_eq!(LiteracyLesson::course_name(&course_manifest), "Course Name");
let course_manifest = CourseManifest {
id: "course_id".into(),
name: "".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: None,
};
assert_eq!(LiteracyLesson::course_name(&course_manifest), "course_id");
}
#[test]
fn lesson_name() {
let lesson = LiteracyLesson {
short_id: Ustr::from("lesson_id"),
dependencies: vec![],
name: Some("Lesson Name".to_string()),
description: None,
instructions: None,
examples: vec![],
exceptions: vec![],
};
assert_eq!(
lesson.lesson_name("Course Name", &LiteracyLessonType::Reading),
"Course Name - Lesson Name - Reading"
);
let lesson = LiteracyLesson {
short_id: Ustr::from("lesson_id"),
dependencies: vec![],
name: None,
description: None,
instructions: None,
examples: vec![],
exceptions: vec![],
};
assert_eq!(
lesson.lesson_name("Course Name", &LiteracyLessonType::Reading),
"Course Name - lesson_id - Reading"
);
}
#[test]
fn open_lesson() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let lesson_dir = temp_dir.path().join("lesson_0.lesson");
fs::create_dir_all(&lesson_dir)?;
fs::write(
lesson_dir.join("lesson.dependencies.json"),
"[\"other_course\"]",
)?;
fs::write(lesson_dir.join("lesson.name.json"), "\"Lesson 0\"")?;
fs::write(
lesson_dir.join("lesson.description.json"),
"\"Description\"",
)?;
fs::write(lesson_dir.join("lesson.instructions.md"), "Instructions")?;
fs::write(lesson_dir.join("example_0.example.md"), "Example 0")?;
fs::write(lesson_dir.join("example_1.example.md"), "Example 1")?;
fs::write(lesson_dir.join("exception_0.exception.md"), "Exception 0")?;
fs::write(lesson_dir.join("exception_1.exception.md"), "Exception 1")?;
fs::write(
lesson_dir.join("simple_examples.md"),
"Simple Example 0\nSimple Example 1",
)?;
fs::write(
lesson_dir.join("simple_exceptions.md"),
"Simple Exception 0\nSimple Exception 1",
)?;
let lesson = LiteracyLesson::open_lesson(&lesson_dir, Ustr::from("lesson_0"))?;
let want = LiteracyLesson {
short_id: Ustr::from("lesson_0"),
dependencies: vec![Ustr::from("other_course")],
name: Some("Lesson 0".to_string()),
description: Some("Description".to_string()),
instructions: Some(BasicAsset::InlinedAsset {
content: "Instructions".to_string(),
}),
examples: vec![
"Example 0".to_string(),
"Example 1".to_string(),
"Simple Example 0".to_string(),
"Simple Example 1".to_string(),
],
exceptions: vec![
"Exception 0".to_string(),
"Exception 1".to_string(),
"Simple Exception 0".to_string(),
"Simple Exception 1".to_string(),
],
};
assert_eq!(lesson, want);
Ok(())
}
fn generate_test_files(
root_dir: &Path,
num_lessons: u8,
num_examples: u8,
num_exceptions: u8,
num_simple_examples: u8,
num_simple_exceptions: u8,
) -> Result<()> {
let course_instructions_file = root_dir.join("course.instructions.md");
fs::write(&course_instructions_file, "# Course Instructions")?;
for i in 0..num_lessons {
let lesson_dir = root_dir.join(format!("lesson_{i}.lesson"));
fs::create_dir_all(&lesson_dir)?;
if i == 0 {
let dependencies_file = lesson_dir.join("lesson.dependencies.json");
let dependencies_content = "[\"other_lesson\"]";
fs::write(&dependencies_file, dependencies_content)?;
} else {
let dependencies_file = lesson_dir.join("lesson.dependencies.json");
let dependencies_content = format!("[\"lesson_{}\", \"other_lesson\"]", i - 1);
fs::write(&dependencies_file, dependencies_content)?;
}
for j in 0..num_examples {
let example_file = lesson_dir.join(format!("example_{j}.example.md"));
let example_content = format!("example_{j}");
fs::write(&example_file, example_content)?;
}
for j in 0..num_exceptions {
let exception_file = lesson_dir.join(format!("exception_{j}.exception.md"));
let exception_content = format!("exception_{j}");
fs::write(&exception_file, exception_content)?;
}
if num_simple_examples > 0 {
let simple_example_file = lesson_dir.join("simple_examples.md");
let simple_example_content = (0..num_simple_examples)
.map(|j| format!("simple_example_{j}"))
.collect::<Vec<_>>()
.join("\n");
fs::write(&simple_example_file, simple_example_content)?;
}
if num_simple_exceptions > 0 {
let simple_exception_file = lesson_dir.join("simple_exceptions.md");
let simple_exception_content = (0..num_simple_exceptions)
.map(|j| format!("simple_exception_{j}"))
.collect::<Vec<_>>()
.join("\n");
fs::write(&simple_exception_file, simple_exception_content)?;
}
}
Ok(())
}
#[test]
fn test_generate_manifests_dictation() -> Result<()> {
let config = CourseGenerator::Literacy(LiteracyConfig {
generate_dictation: true,
});
let course_manifest = CourseManifest {
id: "literacy_course".into(),
name: "Literacy Course".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: Some(config.clone()),
};
let temp_dir = tempfile::tempdir()?;
generate_test_files(temp_dir.path(), 2, 2, 2, 2, 2)?;
let prefs = UserPreferences::default();
let mut got = config.generate_manifests(temp_dir.path(), &course_manifest, &prefs)?;
got.lessons.sort_by(|a, b| a.0.id.cmp(&b.0.id));
for (_, exercises) in &mut got.lessons {
exercises.sort_by(|a, b| a.id.cmp(&b.id));
}
let want = GeneratedCourse {
lessons: vec![
(
LessonManifest {
id: "literacy_course::lesson_0::dictation".into(),
dependencies: vec!["literacy_course::lesson_0::reading".into()],
encompassed: vec![],
superseded: vec![],
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_0 - Dictation".into(),
description: None,
metadata: Some(BTreeMap::from([(
"literacy_lesson".to_string(),
vec!["dictation".to_string()],
)])),
lesson_material: None,
lesson_instructions: None,
},
vec![ExerciseManifest {
id: "literacy_course::lesson_0::dictation::exercise".into(),
lesson_id: "literacy_course::lesson_0::dictation".into(),
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_0 - Dictation".into(),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Dictation,
examples: vec![
"example_0".to_string(),
"example_1".to_string(),
"simple_example_0".to_string(),
"simple_example_1".to_string(),
],
exceptions: vec![
"exception_0".to_string(),
"exception_1".to_string(),
"simple_exception_0".to_string(),
"simple_exception_1".to_string(),
],
},
}],
),
(
LessonManifest {
id: "literacy_course::lesson_0::reading".into(),
dependencies: vec!["other_lesson".into()],
encompassed: vec![],
superseded: vec![],
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_0 - Reading".into(),
description: None,
metadata: Some(BTreeMap::from([(
"literacy_lesson".to_string(),
vec!["reading".to_string()],
)])),
lesson_material: None,
lesson_instructions: None,
},
vec![ExerciseManifest {
id: "literacy_course::lesson_0::reading::exercise".into(),
lesson_id: "literacy_course::lesson_0::reading".into(),
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_0 - Reading".into(),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Reading,
examples: vec![
"example_0".to_string(),
"example_1".to_string(),
"simple_example_0".to_string(),
"simple_example_1".to_string(),
],
exceptions: vec![
"exception_0".to_string(),
"exception_1".to_string(),
"simple_exception_0".to_string(),
"simple_exception_1".to_string(),
],
},
}],
),
(
LessonManifest {
id: "literacy_course::lesson_1::dictation".into(),
dependencies: vec![
"literacy_course::lesson_0::dictation".into(),
"literacy_course::lesson_1::reading".into(),
],
encompassed: vec![],
superseded: vec![],
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_1 - Dictation".into(),
description: None,
metadata: Some(BTreeMap::from([(
"literacy_lesson".to_string(),
vec!["dictation".to_string()],
)])),
lesson_material: None,
lesson_instructions: None,
},
vec![ExerciseManifest {
id: "literacy_course::lesson_1::dictation::exercise".into(),
lesson_id: "literacy_course::lesson_1::dictation".into(),
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_1 - Dictation".into(),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Dictation,
examples: vec![
"example_0".to_string(),
"example_1".to_string(),
"simple_example_0".to_string(),
"simple_example_1".to_string(),
],
exceptions: vec![
"exception_0".to_string(),
"exception_1".to_string(),
"simple_exception_0".to_string(),
"simple_exception_1".to_string(),
],
},
}],
),
(
LessonManifest {
id: "literacy_course::lesson_1::reading".into(),
dependencies: vec![
"literacy_course::lesson_0::reading".into(),
"other_lesson".into(),
],
encompassed: vec![],
superseded: vec![],
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_1 - Reading".into(),
description: None,
metadata: Some(BTreeMap::from([(
"literacy_lesson".to_string(),
vec!["reading".to_string()],
)])),
lesson_material: None,
lesson_instructions: None,
},
vec![ExerciseManifest {
id: "literacy_course::lesson_1::reading::exercise".into(),
lesson_id: "literacy_course::lesson_1::reading".into(),
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_1 - Reading".into(),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Reading,
examples: vec![
"example_0".to_string(),
"example_1".to_string(),
"simple_example_0".to_string(),
"simple_example_1".to_string(),
],
exceptions: vec![
"exception_0".to_string(),
"exception_1".to_string(),
"simple_exception_0".to_string(),
"simple_exception_1".to_string(),
],
},
}],
),
],
updated_metadata: Some(BTreeMap::from([(
"literacy_course".to_string(),
vec!["true".to_string()],
)])),
updated_instructions: Some(BasicAsset::InlinedAsset {
content: "# Course Instructions".to_string(),
}),
};
assert_eq!(got, want);
Ok(())
}
#[test]
fn test_generate_manifests_no_dictation() -> Result<()> {
let config = CourseGenerator::Literacy(LiteracyConfig {
generate_dictation: false,
});
let course_manifest = CourseManifest {
id: "literacy_course".into(),
name: "Literacy Course".into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: Some(config.clone()),
};
let temp_dir = tempfile::tempdir()?;
generate_test_files(temp_dir.path(), 2, 2, 2, 2, 2)?;
let prefs = UserPreferences::default();
let mut got = config.generate_manifests(temp_dir.path(), &course_manifest, &prefs)?;
got.lessons.sort_by(|a, b| a.0.id.cmp(&b.0.id));
for (_, exercises) in &mut got.lessons {
exercises.sort_by(|a, b| a.id.cmp(&b.id));
}
let want = GeneratedCourse {
lessons: vec![
(
LessonManifest {
id: "literacy_course::lesson_0::reading".into(),
dependencies: vec!["other_lesson".into()],
encompassed: vec![],
superseded: vec![],
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_0 - Reading".into(),
description: None,
metadata: Some(BTreeMap::from([(
"literacy_lesson".to_string(),
vec!["reading".to_string()],
)])),
lesson_material: None,
lesson_instructions: None,
},
vec![ExerciseManifest {
id: "literacy_course::lesson_0::reading::exercise".into(),
lesson_id: "literacy_course::lesson_0::reading".into(),
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_0 - Reading".into(),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Reading,
examples: vec![
"example_0".to_string(),
"example_1".to_string(),
"simple_example_0".to_string(),
"simple_example_1".to_string(),
],
exceptions: vec![
"exception_0".to_string(),
"exception_1".to_string(),
"simple_exception_0".to_string(),
"simple_exception_1".to_string(),
],
},
}],
),
(
LessonManifest {
id: "literacy_course::lesson_1::reading".into(),
dependencies: vec![
"literacy_course::lesson_0::reading".into(),
"other_lesson".into(),
],
encompassed: vec![],
superseded: vec![],
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_1 - Reading".into(),
description: None,
metadata: Some(BTreeMap::from([(
"literacy_lesson".to_string(),
vec!["reading".to_string()],
)])),
lesson_material: None,
lesson_instructions: None,
},
vec![ExerciseManifest {
id: "literacy_course::lesson_1::reading::exercise".into(),
lesson_id: "literacy_course::lesson_1::reading".into(),
course_id: "literacy_course".into(),
name: "Literacy Course - lesson_1 - Reading".into(),
description: None,
exercise_type: ExerciseType::Procedural,
exercise_asset: ExerciseAsset::LiteracyAsset {
lesson_type: LiteracyLessonType::Reading,
examples: vec![
"example_0".to_string(),
"example_1".to_string(),
"simple_example_0".to_string(),
"simple_example_1".to_string(),
],
exceptions: vec![
"exception_0".to_string(),
"exception_1".to_string(),
"simple_exception_0".to_string(),
"simple_exception_1".to_string(),
],
},
}],
),
],
updated_metadata: Some(BTreeMap::from([(
"literacy_course".to_string(),
vec!["true".to_string()],
)])),
updated_instructions: Some(BasicAsset::InlinedAsset {
content: "# Course Instructions".to_string(),
}),
};
assert_eq!(got, want);
Ok(())
}
}