4.1 KiB
4.1 KiB
Task 2 — CurriculumLoader (OnceLock-cached, no expect())
Goal
Provide a lazy, cached accessor over the embedded curriculum that returns Result<&Curriculum, BridgeError> instead of panicking. Required by every tutor tool. CLAUDE.md §10 forbids expect() in production.
Prerequisites
- Task 1 merged.
Files
- Modify:
src/tutor.rs - Modify:
src/error.rs
Steps
- Step 1: Add a
Curriculumvariant toBridgeErrorinsrc/error.rs
#[error("curriculum data is malformed: {0}")]
Curriculum(String),
- Step 2: Append failing tests to
src/tutor.rs(inside the existing#[cfg(test)]block)
#[test]
fn loader_returns_curriculum_on_first_call() {
let loader = CurriculumLoader::default();
let c = loader.curriculum().expect("embedded curriculum must parse");
assert_eq!(c.version, "1.0");
}
#[test]
fn find_exercise_by_id_returns_correct_exercise() {
let loader = CurriculumLoader::default();
let exercise = loader.find_exercise("1-1-a").unwrap();
assert_eq!(exercise.id, "1-1-a");
}
#[test]
fn find_exercise_with_unknown_id_returns_none() {
let loader = CurriculumLoader::default();
assert!(loader.find_exercise("99-99-z").is_none());
}
#[test]
fn get_lesson_unknown_module_returns_none() {
let loader = CurriculumLoader::default();
assert!(loader.get_lesson(99, 1).is_none());
}
#[test]
fn loader_reports_curriculum_error_for_malformed_json() {
let result = CurriculumLoader::from_str("{ not valid json");
assert!(result.is_err());
}
- Step 3: Implement
CurriculumLoader(above the#[cfg(test)]block)
use std::sync::OnceLock;
use crate::error::BridgeError;
const CURRICULUM_JSON: &str = include_str!("../curriculum/curriculum.json");
#[derive(Default)]
pub struct CurriculumLoader {
cell: OnceLock<Result<Curriculum, String>>,
}
impl CurriculumLoader {
pub fn curriculum(&self) -> Result<&Curriculum, BridgeError> {
let result = self.cell.get_or_init(|| {
serde_json::from_str::<Curriculum>(CURRICULUM_JSON).map_err(|e| e.to_string())
});
result
.as_ref()
.map_err(|msg| BridgeError::Curriculum(msg.clone()))
}
pub fn from_str(json: &str) -> Result<Self, BridgeError> {
let curriculum: Curriculum =
serde_json::from_str(json).map_err(|e| BridgeError::Curriculum(e.to_string()))?;
let loader = Self::default();
let _ = loader.cell.set(Ok(curriculum));
Ok(loader)
}
pub fn get_module(&self, module_id: u32) -> Option<&Module> {
self.curriculum().ok()?.modules.iter().find(|m| m.id == module_id)
}
pub fn get_lesson(&self, module_id: u32, lesson_id: u32) -> Option<&Lesson> {
self.get_module(module_id)?
.lessons
.iter()
.find(|l| l.id == lesson_id)
}
pub fn find_exercise(&self, exercise_id: &str) -> Option<&Exercise> {
for module in &self.curriculum().ok()?.modules {
for lesson in &module.lessons {
if let Some(ex) = lesson.exercises.iter().find(|e| e.id == exercise_id) {
return Some(ex);
}
}
}
None
}
pub fn all_exercises(&self) -> Vec<&Exercise> {
self.curriculum()
.ok()
.map(|c| {
c.modules
.iter()
.flat_map(|m| m.lessons.iter())
.flat_map(|l| l.exercises.iter())
.collect()
})
.unwrap_or_default()
}
}
- Step 4: Run tests (Green)
cargo test tutor::tests 2>&1 | grep -E "test result|FAILED"
Expected: test result: ok. 6 passed.
- Step 5: Commit
git add src/tutor.rs src/error.rs
git commit -m "feat: implement CurriculumLoader with OnceLock cache and structured error"