pub mod knowledge_base_builder;
use anyhow::{Context, Result, ensure};
use serde::{Deserialize, Serialize};
use std::{
fs::{File, create_dir_all},
io::Write,
path::{Path, PathBuf},
};
use strum::Display;
use crate::data::{CourseManifest, ExerciseManifestBuilder, LessonManifestBuilder, VerifyPaths};
#[derive(Display)]
#[strum(serialize_all = "snake_case")]
#[allow(missing_docs)]
pub enum TraneMetadata {
Skill,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AssetBuilder {
pub file_name: String,
pub contents: String,
}
impl AssetBuilder {
pub fn build(&self, asset_directory: &Path) -> Result<()> {
create_dir_all(asset_directory)?;
let asset_path = asset_directory.join(&self.file_name);
ensure!(
!asset_path.exists(),
"asset path {} already exists",
asset_path.display()
);
create_dir_all(asset_path.parent().unwrap())?;
let mut asset_file = File::create(asset_path)?;
asset_file.write_all(self.contents.as_bytes())?;
Ok(())
}
}
pub struct ExerciseBuilder {
pub directory_name: String,
pub manifest_closure: Box<dyn Fn(ExerciseManifestBuilder) -> ExerciseManifestBuilder>,
pub asset_builders: Vec<AssetBuilder>,
}
impl ExerciseBuilder {
pub fn build(
&self,
exercise_directory: &PathBuf,
manifest_template: ExerciseManifestBuilder,
) -> Result<()> {
create_dir_all(exercise_directory)?;
let manifest = (self.manifest_closure)(manifest_template).build()?;
let manifest_json = serde_json::to_string_pretty(&manifest)? + "\n";
let manifest_path = exercise_directory.join("exercise_manifest.json");
let mut manifest_file = File::create(manifest_path)?;
manifest_file.write_all(manifest_json.as_bytes())?;
for asset_builder in &self.asset_builders {
asset_builder.build(exercise_directory)?;
}
manifest.verify_paths(exercise_directory).context(format!(
"failed to verify files for exercise {}",
manifest.id
))?;
Ok(())
}
}
pub struct LessonBuilder {
pub directory_name: String,
pub manifest_closure: Box<dyn Fn(LessonManifestBuilder) -> LessonManifestBuilder>,
pub exercise_manifest_template: ExerciseManifestBuilder,
pub exercise_builders: Vec<ExerciseBuilder>,
pub asset_builders: Vec<AssetBuilder>,
}
impl LessonBuilder {
pub fn build(
&self,
lesson_directory: &PathBuf,
manifest_template: LessonManifestBuilder,
) -> Result<()> {
create_dir_all(lesson_directory)?;
let manifest = (self.manifest_closure)(manifest_template).build()?;
let manifest_json = serde_json::to_string_pretty(&manifest)? + "\n";
let manifest_path = lesson_directory.join("lesson_manifest.json");
let mut manifest_file = File::create(manifest_path)?;
manifest_file.write_all(manifest_json.as_bytes())?;
for asset_builder in &self.asset_builders {
asset_builder.build(lesson_directory)?;
}
for exercise_builder in &self.exercise_builders {
let exercise_directory = lesson_directory.join(&exercise_builder.directory_name);
exercise_builder.build(&exercise_directory, self.exercise_manifest_template.clone())?;
}
manifest
.verify_paths(lesson_directory)
.context(format!("failed to verify files for lesson {}", manifest.id))?;
Ok(())
}
}
pub struct CourseBuilder {
pub directory_name: String,
pub course_manifest: CourseManifest,
pub lesson_manifest_template: LessonManifestBuilder,
pub lesson_builders: Vec<LessonBuilder>,
pub asset_builders: Vec<AssetBuilder>,
}
impl CourseBuilder {
pub fn build(&self, parent_directory: &Path) -> Result<()> {
let course_directory = parent_directory.join(&self.directory_name);
create_dir_all(&course_directory)?;
let manifest_json = serde_json::to_string_pretty(&self.course_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())?;
for asset_builder in &self.asset_builders {
asset_builder.build(&course_directory)?;
}
for lesson_builder in &self.lesson_builders {
let lesson_directory = course_directory.join(&lesson_builder.directory_name);
lesson_builder.build(&lesson_directory, self.lesson_manifest_template.clone())?;
}
self.course_manifest
.verify_paths(&course_directory)
.context(format!(
"failed to verify files for course {}",
self.course_manifest.id
))?;
Ok(())
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::Result;
use std::io::Read;
use super::*;
use crate::data::{BasicAsset, ExerciseAsset, ExerciseType};
#[test]
fn asset_builer() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let asset_builder = AssetBuilder {
file_name: "asset1.md".to_string(),
contents: "asset1 contents".to_string(),
};
asset_builder.build(temp_dir.path())?;
assert!(temp_dir.path().join("asset1.md").is_file());
let mut file = File::open(temp_dir.path().join("asset1.md"))?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
assert_eq!(contents, "asset1 contents");
Ok(())
}
#[test]
fn asset_builer_existing() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let asset_path = temp_dir.path().join("asset1.md");
File::create(&asset_path)?;
let asset_builder = AssetBuilder {
file_name: "asset1.md".to_string(),
contents: "asset1 contents".to_string(),
};
assert!(asset_builder.build(temp_dir.path()).is_err());
Ok(())
}
#[test]
fn course_builder() -> Result<()> {
let exercise_builder = ExerciseBuilder {
directory_name: "exercise1".to_string(),
manifest_closure: Box::new(|builder| {
builder
.clone()
.id("exercise1")
.name("Exercise 1".into())
.exercise_asset(ExerciseAsset::BasicAsset(BasicAsset::InlinedAsset {
content: String::new(),
}))
.clone()
}),
asset_builders: vec![],
};
let lesson_builder = LessonBuilder {
directory_name: "lesson1".to_string(),
manifest_closure: Box::new(|builder| {
builder
.clone()
.id("lesson1")
.name("Lesson 1".into())
.dependencies(vec![])
.clone()
}),
exercise_manifest_template: ExerciseManifestBuilder::default()
.lesson_id("lesson1")
.course_id("course1")
.exercise_type(ExerciseType::Procedural)
.clone(),
exercise_builders: vec![exercise_builder],
asset_builders: vec![],
};
let course_builder = CourseBuilder {
directory_name: "course1".to_string(),
course_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,
},
lesson_manifest_template: LessonManifestBuilder::default()
.course_id("course1")
.clone(),
lesson_builders: vec![lesson_builder],
asset_builders: vec![],
};
let temp_dir = tempfile::tempdir()?;
course_builder.build(temp_dir.path())?;
let course_dir = temp_dir.path().join("course1");
let lesson_dir = course_dir.join("lesson1");
let exercise_dir = lesson_dir.join("exercise1");
assert!(course_dir.is_dir());
assert!(lesson_dir.is_dir());
assert!(exercise_dir.is_dir());
assert!(course_dir.join("course_manifest.json").is_file());
assert!(lesson_dir.join("lesson_manifest.json").is_file());
assert!(exercise_dir.join("exercise_manifest.json").is_file());
Ok(())
}
#[test]
fn trane_metadata_display() {
assert_eq!("skill", TraneMetadata::Skill.to_string());
}
}