Initial import
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Task 2 — `CurriculumLoader` (`OnceLock`-cached, no `expect()`)
|
||||
|
||||
> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md).
|
||||
|
||||
## 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 `Curriculum` variant to `BridgeError` in `src/error.rs`**
|
||||
|
||||
```rust
|
||||
#[error("curriculum data is malformed: {0}")]
|
||||
Curriculum(String),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Append failing tests to `src/tutor.rs` (inside the existing `#[cfg(test)]` block)**
|
||||
|
||||
```rust
|
||||
#[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)**
|
||||
|
||||
```rust
|
||||
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)**
|
||||
|
||||
```bash
|
||||
cargo test tutor::tests 2>&1 | grep -E "test result|FAILED"
|
||||
```
|
||||
|
||||
Expected: `test result: ok. 6 passed`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/tutor.rs src/error.rs
|
||||
git commit -m "feat: implement CurriculumLoader with OnceLock cache and structured error"
|
||||
```
|
||||
Reference in New Issue
Block a user