• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

trane-project / trane / 14392837286

11 Apr 2025 12:23AM UTC coverage: 99.221% (-0.4%) from 99.655%
14392837286

Pull #340

github

web-flow
Merge 48f74d0a2 into e7a732a69
Pull Request #340: Fixes to support external applications

24 of 29 new or added lines in 3 files covered. (82.76%)

19 existing lines in 4 files now uncovered.

5478 of 5521 relevant lines covered (99.22%)

172767.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.46
/src/course_library.rs
1
//! Defines the operations that can be performed on a collection of courses stored by the student.
2
//!
3
//! A course library (the term Trane library will be used interchangeably) is a collection of
4
//! courses that the student wishes to practice together. Courses, lessons, and exercises are
5
//! defined by their manifest files (see [data](crate::data)).
6

7
use anyhow::{Context, Result, anyhow, ensure};
8
use parking_lot::RwLock;
9
use serde::de::DeserializeOwned;
10
use std::{
11
    collections::BTreeMap,
12
    fs::File,
13
    io::BufReader,
14
    path::{self, Path},
15
    sync::Arc,
16
};
17
use tantivy::{
18
    Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument,
19
    collector::TopDocs,
20
    doc,
21
    query::QueryParser,
22
    schema::{Field, STORED, Schema, TEXT, Value},
23
};
24
use ustr::{Ustr, UstrMap, UstrSet};
25
use walkdir::WalkDir;
26

27
use crate::{
28
    data::{
29
        CourseManifest, ExerciseManifest, GenerateManifests, LessonManifest, NormalizePaths,
30
        UnitType, UserPreferences,
31
    },
32
    error::CourseLibraryError,
33
    graph::{InMemoryUnitGraph, UnitGraph},
34
};
35

36
/// The file name for all course manifests.
37
pub const COURSE_MANIFEST_FILENAME: &str = "course_manifest.json";
38

39
/// The file name for all lesson manifests.
40
pub const LESSON_MANIFEST_FILENAME: &str = "lesson_manifest.json";
41

42
/// The file name for all exercise manifests.
43
pub const EXERCISE_MANIFEST_FILENAME: &str = "exercise_manifest.json";
44

45
/// The name of the field for the unit ID in the search schema.
46
const ID_SCHEMA_FIELD: &str = "id";
47

48
/// The name of the field for the unit name in the search schema.
49
const NAME_SCHEMA_FIELD: &str = "name";
50

51
/// The name of the field for the unit description in the search schema.
52
const DESCRIPTION_SCHEMA_FIELD: &str = "description";
53

54
/// The name of the field for the unit metadata in the search schema.
55
const METADATA_SCHEMA_FIELD: &str = "metadata";
56

57
/// A trait that manages a course library, its corresponding manifest files, and provides basic
58
/// operations to retrieve the courses, lessons in a course, and exercises in a lesson.
59
pub trait CourseLibrary {
60
    /// Returns the course manifest for the given course.
61
    fn get_course_manifest(&self, course_id: Ustr) -> Option<CourseManifest>;
62

63
    /// Returns the lesson manifest for the given lesson.
64
    fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<LessonManifest>;
65

66
    /// Returns the exercise manifest for the given exercise.
67
    fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<ExerciseManifest>;
68

69
    /// Returns the IDs of all courses in the library sorted alphabetically.
70
    fn get_course_ids(&self) -> Vec<Ustr>;
71

72
    /// Returns the IDs of all lessons in the given course sorted alphabetically.
73
    fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>>;
74

75
    /// Returns the IDs of all exercises in the given lesson sorted alphabetically.
76
    fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>>;
77

78
    /// Returns the IDs of all exercises in the given course sorted alphabetically.
79
    fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr>;
80

81
    /// Returns the set of units whose ID starts with the given prefix and are of the given type.
82
    /// If `unit_type` is `None`, then all unit types are considered.
83
    fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet;
84

85
    /// Returns the IDs of all the units which match the given query.
86
    fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError>;
87
}
88

89
/// A trait that retrieves the unit graph generated after reading a course library.
90
pub(crate) trait GetUnitGraph {
91
    /// Returns a reference to the in-memory unit graph describing the dependencies among the
92
    /// courses and lessons in this library.
93
    fn get_unit_graph(&self) -> Arc<RwLock<InMemoryUnitGraph>>;
94
}
95

96
/// An implementation of [`CourseLibrary`] backed by the local file system. The courses in this
97
/// library are those directories located anywhere under the given root directory that match the
98
/// following structure:
99
///
100
/// ```text
101
/// course-manifest.json
102
/// <LESSON_DIR_1>/
103
///     lesson-manifest.json
104
///    <EXERCISE_DIR_1>/
105
///       exercise-manifest.json
106
///   <EXERCISE_DIR_2>/
107
///      exercise-manifest.json
108
///    ...
109
/// <LESSON_DIR_2>/
110
///    lesson-manifest.json
111
///   <EXERCISE_DIR_1>/
112
///     exercise-manifest.json
113
///   ...
114
/// ```
115
///
116
/// The directory can also contain asset files referenced by the manifests. For example, a basic
117
/// flashcard with a front and back can be stored using two markdown files.
118
pub struct LocalCourseLibrary {
119
    /// A `UnitGraph` constructed when opening the library.
120
    unit_graph: Arc<RwLock<InMemoryUnitGraph>>,
121

122
    /// A mapping of course ID to its corresponding course manifest.
123
    course_map: UstrMap<CourseManifest>,
124

125
    /// A mapping of lesson ID to its corresponding lesson manifest.
126
    lesson_map: UstrMap<LessonManifest>,
127

128
    /// A mapping of exercise ID to its corresponding exercise manifest.
129
    exercise_map: UstrMap<ExerciseManifest>,
130

131
    /// The user preferences.
132
    user_preferences: UserPreferences,
133

134
    /// A tantivy index used for searching the course library.
135
    index: Index,
136

137
    /// A reader to access the search index.
138
    reader: Option<IndexReader>,
139
}
140

141
impl LocalCourseLibrary {
142
    /// Returns the tantivy schema used for searching the course library.
143
    fn search_schema() -> Schema {
62,519✔
144
        let mut schema = Schema::builder();
62,519✔
145
        schema.add_text_field(ID_SCHEMA_FIELD, TEXT | STORED);
62,519✔
146
        schema.add_text_field(NAME_SCHEMA_FIELD, TEXT | STORED);
62,519✔
147
        schema.add_text_field(DESCRIPTION_SCHEMA_FIELD, TEXT | STORED);
62,519✔
148
        schema.add_text_field(METADATA_SCHEMA_FIELD, TEXT | STORED);
62,519✔
149
        schema.build()
62,519✔
150
    }
62,519✔
151

152
    /// Returns the field in the search schema with the given name.
153
    fn schema_field(field_name: &str) -> Result<Field> {
62,448✔
154
        let schema = Self::search_schema();
62,448✔
155
        let field = schema.get_field(field_name)?;
62,448✔
156
        Ok(field)
62,448✔
157
    }
62,448✔
158

159
    /// Adds the unit with the given field values to the search index.
160
    fn add_to_index_writer(
15,601✔
161
        index_writer: &mut IndexWriter,
15,601✔
162
        id: Ustr,
15,601✔
163
        name: &str,
15,601✔
164
        description: Option<&str>,
15,601✔
165
        metadata: Option<&BTreeMap<String, Vec<String>>>,
15,601✔
166
    ) -> Result<()> {
15,601✔
167
        // Extract the description from the `Option` value to satisfy the borrow checker.
15,601✔
168
        let empty = String::new();
15,601✔
169
        let description = description.unwrap_or(&empty);
15,601✔
170

171
        // Declare the base document with the ID, name, and description fields.
172
        let mut doc = doc!(
15,601✔
173
            Self::schema_field(ID_SCHEMA_FIELD)? => id.to_string(),
15,601✔
174
            Self::schema_field(NAME_SCHEMA_FIELD)? => name.to_string(),
15,601✔
175
            Self::schema_field(DESCRIPTION_SCHEMA_FIELD)? => description.to_string(),
15,601✔
176
        );
177

178
        // Add the metadata. Encode each key-value pair as a string in the format "key:value". Then
179
        // add the document to the index.
180
        let metadata_field = Self::schema_field(METADATA_SCHEMA_FIELD)?;
15,601✔
181
        if let Some(metadata) = metadata {
15,601✔
182
            for (key, values) in metadata {
2,421✔
183
                for value in values {
996✔
184
                    doc.add_text(metadata_field, format!("{key}:{value}"));
498✔
185
                }
498✔
186
            }
187
        }
13,678✔
188
        index_writer.add_document(doc)?;
15,601✔
189
        Ok(())
15,601✔
190
    }
15,601✔
191

192
    /// Opens the course, lesson, or exercise manifest located at the given path.
193
    fn open_manifest<T: DeserializeOwned>(path: &Path) -> Result<T> {
14,829✔
194
        let display = path.display();
14,829✔
195
        let file = File::open(path).context(format!("cannot open manifest file {display}"))?;
14,829✔
196
        let reader = BufReader::new(file);
14,829✔
197
        serde_json::from_reader(reader).context(format!("cannot parse manifest file {display}"))
14,829✔
198
    }
14,829✔
199

200
    /// Returns the file name of the given path.
201
    fn get_file_name(path: &Path) -> Result<String> {
90,070✔
202
        Ok(path
90,070✔
203
            .file_name()
90,070✔
204
            .ok_or(anyhow!("cannot get file name from DirEntry"))?
90,070✔
205
            .to_str()
90,070✔
206
            .ok_or(anyhow!("invalid dir entry {}", path.display()))?
90,070✔
207
            .to_string())
90,070✔
208
    }
90,070✔
209

210
    // Verifies that the IDs mentioned in the exercise manifest and its lesson manifest are valid
211
    // and agree with each other.
212
    #[cfg_attr(coverage, coverage(off))]
213
    fn verify_exercise_manifest(
214
        lesson_manifest: &LessonManifest,
215
        exercise_manifest: &ExerciseManifest,
216
    ) -> Result<()> {
217
        ensure!(!exercise_manifest.id.is_empty(), "ID in manifest is empty");
218
        ensure!(
219
            exercise_manifest.lesson_id == lesson_manifest.id,
220
            "lesson_id in manifest for exercise {} does not match the manifest for lesson {}",
221
            exercise_manifest.id,
222
            lesson_manifest.id,
223
        );
224
        ensure!(
225
            exercise_manifest.course_id == lesson_manifest.course_id,
226
            "course_id in manifest for exercise {} does not match the manifest for course {}",
227
            exercise_manifest.id,
228
            lesson_manifest.course_id,
229
        );
230
        Ok(())
231
    }
232

233
    /// Adds an exercise to the course library given its manifest and the manifest of the lesson to
234
    /// which it belongs.
235
    fn process_exercise_manifest(
13,606✔
236
        &mut self,
13,606✔
237
        lesson_manifest: &LessonManifest,
13,606✔
238
        exercise_manifest: ExerciseManifest,
13,606✔
239
        index_writer: &mut IndexWriter,
13,606✔
240
    ) -> Result<()> {
13,606✔
241
        LocalCourseLibrary::verify_exercise_manifest(lesson_manifest, &exercise_manifest)?;
13,606✔
242

243
        // Add the exercise manifest to the search index.
244
        Self::add_to_index_writer(
13,606✔
245
            index_writer,
13,606✔
246
            exercise_manifest.id,
13,606✔
247
            &exercise_manifest.name,
13,606✔
248
            exercise_manifest.description.as_deref(),
13,606✔
249
            None,
13,606✔
UNCOV
250
        )?;
×
251

252
        // Add the exercise to the unit graph and exercise map.
253
        self.unit_graph
13,606✔
254
            .write()
13,606✔
255
            .add_exercise(exercise_manifest.id, exercise_manifest.lesson_id)?;
13,606✔
256
        self.exercise_map
13,606✔
257
            .insert(exercise_manifest.id, exercise_manifest);
13,606✔
258
        Ok(())
13,606✔
259
    }
13,606✔
260

261
    /// Verifes that the IDs mentioned in the lesson manifest and its course manifestsare valid and
262
    /// agree with each other.
263
    #[cfg_attr(coverage, coverage(off))]
264
    fn verify_lesson_manifest(
265
        course_manifest: &CourseManifest,
266
        lesson_manifest: &LessonManifest,
267
    ) -> Result<()> {
268
        // Verify that the IDs mentioned in the manifests are valid and agree with each other.
269
        ensure!(!lesson_manifest.id.is_empty(), "ID in manifest is empty",);
270
        ensure!(
271
            lesson_manifest.course_id == course_manifest.id,
272
            "course_id in manifest for lesson {} does not match the manifest for course {}",
273
            lesson_manifest.id,
274
            course_manifest.id,
275
        );
276
        Ok(())
277
    }
278

279
    /// Adds a lesson to the course library given its manifest and the manifest of the course to
280
    /// which it belongs. It also traverses the given `DirEntry` and adds all the exercises in the
281
    /// lesson.
282
    fn process_lesson_manifest(
1,517✔
283
        &mut self,
1,517✔
284
        lesson_root: &Path,
1,517✔
285
        course_manifest: &CourseManifest,
1,517✔
286
        lesson_manifest: &LessonManifest,
1,517✔
287
        index_writer: &mut IndexWriter,
1,517✔
288
        generated_exercises: Option<&Vec<ExerciseManifest>>,
1,517✔
289
    ) -> Result<()> {
1,517✔
290
        LocalCourseLibrary::verify_lesson_manifest(course_manifest, lesson_manifest)?;
1,517✔
291

292
        // Add the lesson, the dependencies, and the superseded units explicitly listed in the
293
        // lesson manifest.
294
        self.unit_graph
1,517✔
295
            .write()
1,517✔
296
            .add_lesson(lesson_manifest.id, lesson_manifest.course_id)?;
1,517✔
297
        self.unit_graph.write().add_dependencies(
1,517✔
298
            lesson_manifest.id,
1,517✔
299
            UnitType::Lesson,
1,517✔
300
            &lesson_manifest.dependencies,
1,517✔
301
        )?;
1,517✔
302
        self.unit_graph
1,517✔
303
            .write()
1,517✔
304
            .add_superseded(lesson_manifest.id, &lesson_manifest.superseded);
1,517✔
305

306
        // Add the generated exercises to the lesson.
307
        if let Some(exercises) = generated_exercises {
1,517✔
308
            for exercise_manifest in exercises {
772✔
309
                let exercise_manifest = &exercise_manifest.normalize_paths(lesson_root)?;
641✔
310
                self.process_exercise_manifest(
641✔
311
                    lesson_manifest,
641✔
312
                    exercise_manifest.clone(),
641✔
313
                    index_writer,
641✔
UNCOV
314
                )?;
×
315
            }
316
        }
1,386✔
317

318
        // Start a new search from the parent of the passed `DirEntry`, which corresponds to the
319
        // lesson's root. Each exercise in the lesson must be contained in a directory that is a
320
        // direct descendant of the lesson's root. Therefore, all the exercise manifests will be
321
        // found at a depth of two.
322
        for entry in WalkDir::new(lesson_root)
40,833✔
323
            .min_depth(2)
1,517✔
324
            .max_depth(2)
1,517✔
325
            .into_iter()
1,517✔
326
            .flatten()
1,517✔
327
        {
328
            // Ignore any entries that are not files named `exercise_manifest.json`.
329
            if entry.path().is_dir() {
40,833✔
330
                continue;
×
331
            }
40,833✔
332
            let file_name = Self::get_file_name(entry.path())?;
40,833✔
333
            if file_name != EXERCISE_MANIFEST_FILENAME {
40,833✔
334
                continue;
27,868✔
335
            }
12,965✔
336

337
            // Open the exercise manifest and process it.
338
            let mut exercise_manifest: ExerciseManifest = Self::open_manifest(entry.path())?;
12,965✔
339
            exercise_manifest =
12,965✔
340
                exercise_manifest.normalize_paths(entry.path().parent().unwrap())?;
12,965✔
341
            self.process_exercise_manifest(lesson_manifest, exercise_manifest, index_writer)?;
12,965✔
342
        }
343

344
        // Add the lesson manifest to the lesson map and the search index.
345
        self.lesson_map
1,517✔
346
            .insert(lesson_manifest.id, lesson_manifest.clone());
1,517✔
347
        Self::add_to_index_writer(
1,517✔
348
            index_writer,
1,517✔
349
            lesson_manifest.id,
1,517✔
350
            &lesson_manifest.name,
1,517✔
351
            lesson_manifest.description.as_deref(),
1,517✔
352
            lesson_manifest.metadata.as_ref(),
1,517✔
UNCOV
353
        )?;
×
354
        Ok(())
1,517✔
355
    }
1,517✔
356

357
    /// Verifies that the IDs mentioned in the course manifest are valid.
358
    #[cfg_attr(coverage, coverage(off))]
359
    fn verify_course_manifest(course_manifest: &CourseManifest) -> Result<()> {
360
        ensure!(!course_manifest.id.is_empty(), "ID in manifest is empty",);
361
        Ok(())
362
    }
363

364
    /// Adds a course to the course library given its manifest. It also traverses the given
365
    /// `DirEntry` and adds all the lessons in the course.
366
    fn process_course_manifest(
478✔
367
        &mut self,
478✔
368
        course_root: &Path,
478✔
369
        mut course_manifest: CourseManifest,
478✔
370
        index_writer: &mut IndexWriter,
478✔
371
    ) -> Result<()> {
478✔
372
        LocalCourseLibrary::verify_course_manifest(&course_manifest)?;
478✔
373

374
        // Add the course, the dependencies, and the superseded units explicitly listed in the
375
        // manifest.
376
        self.unit_graph.write().add_course(course_manifest.id)?;
478✔
377
        self.unit_graph.write().add_dependencies(
478✔
378
            course_manifest.id,
478✔
379
            UnitType::Course,
478✔
380
            &course_manifest.dependencies,
478✔
381
        )?;
478✔
382
        self.unit_graph
478✔
383
            .write()
478✔
384
            .add_superseded(course_manifest.id, &course_manifest.superseded);
478✔
385

386
        // If the course has a generator config, generate the lessons and exercises and add them to
387
        // the library.
388
        if let Some(generator_config) = &course_manifest.generator_config {
478✔
389
            let generated_course = generator_config.generate_manifests(
19✔
390
                course_root,
19✔
391
                &course_manifest,
19✔
392
                &self.user_preferences,
19✔
UNCOV
393
            )?;
×
394
            for (lesson_manifest, exercise_manifests) in generated_course.lessons {
150✔
395
                // All the generated lessons will use the root of the course as their root.
396
                self.process_lesson_manifest(
131✔
397
                    course_root,
131✔
398
                    &course_manifest,
131✔
399
                    &lesson_manifest,
131✔
400
                    index_writer,
131✔
401
                    Some(&exercise_manifests),
131✔
UNCOV
402
                )?;
×
403
            }
404

405
            // Update the course manifest's metadata, material, and instructions if needed.
406
            if generated_course.updated_metadata.is_some() {
19✔
407
                course_manifest.metadata = generated_course.updated_metadata;
12✔
408
            }
12✔
409
            if generated_course.updated_instructions.is_some() {
19✔
410
                course_manifest.course_instructions = generated_course.updated_instructions;
12✔
411
            }
12✔
412
        }
459✔
413

414
        // Start a new search from the parent of the passed `DirEntry`, which corresponds to the
415
        // course's root. Each lesson in the course must be contained in a directory that is a
416
        // direct descendant of its root. Therefore, all the lesson manifests will be found at a
417
        // depth of two.
418
        for entry in WalkDir::new(course_root)
17,333✔
419
            .min_depth(2)
478✔
420
            .max_depth(2)
478✔
421
            .into_iter()
478✔
422
            .flatten()
478✔
423
        {
424
            // Ignore any entries which are not directories.
425
            if entry.path().is_dir() {
17,333✔
426
                continue;
12,965✔
427
            }
4,368✔
428

429
            // Ignore any files which are not named `lesson_manifest.json`.
430
            let file_name = Self::get_file_name(entry.path())?;
4,368✔
431
            if file_name != LESSON_MANIFEST_FILENAME {
4,368✔
432
                continue;
2,982✔
433
            }
1,386✔
434

435
            // Open the lesson manifest and process it.
436
            let mut lesson_manifest: LessonManifest = Self::open_manifest(entry.path())?;
1,386✔
437
            lesson_manifest = lesson_manifest.normalize_paths(entry.path().parent().unwrap())?;
1,386✔
438
            self.process_lesson_manifest(
1,386✔
439
                entry.path().parent().unwrap(),
1,386✔
440
                &course_manifest,
1,386✔
441
                &lesson_manifest,
1,386✔
442
                index_writer,
1,386✔
443
                None,
1,386✔
UNCOV
444
            )?;
×
445
        }
446

447
        // Add the course manifest to the course map and the search index. This needs to happen at
448
        // the end in case the course has a generator config and the course manifest was updated.
449
        self.course_map
478✔
450
            .insert(course_manifest.id, course_manifest.clone());
478✔
451
        Self::add_to_index_writer(
478✔
452
            index_writer,
478✔
453
            course_manifest.id,
478✔
454
            &course_manifest.name,
478✔
455
            course_manifest.description.as_deref(),
478✔
456
            course_manifest.metadata.as_ref(),
478✔
UNCOV
457
        )?;
×
458
        Ok(())
478✔
459
    }
478✔
460

461
    /// A constructor taking the path to the root of the library.
462
    pub fn new(library_root: &Path, user_preferences: UserPreferences) -> Result<Self> {
71✔
463
        let mut library = LocalCourseLibrary {
71✔
464
            course_map: UstrMap::default(),
71✔
465
            lesson_map: UstrMap::default(),
71✔
466
            exercise_map: UstrMap::default(),
71✔
467
            user_preferences,
71✔
468
            unit_graph: Arc::new(RwLock::new(InMemoryUnitGraph::default())),
71✔
469
            index: Index::create_in_ram(Self::search_schema()),
71✔
470
            reader: None,
71✔
471
        };
71✔
472

473
        // Initialize the search index writer with an initial arena size of 150 MB.
474
        let mut index_writer = library.index.writer(150_000_000)?;
71✔
475

476
        // Convert the list of paths to ignore into absolute paths.
477
        let absolute_root = path::absolute(library_root)?;
71✔
478
        let ignored_paths = library
71✔
479
            .user_preferences
71✔
480
            .ignored_paths
71✔
481
            .iter()
71✔
482
            .map(|path| {
71✔
483
                let mut absolute_path = absolute_root.clone();
2✔
484
                absolute_path.push(path);
2✔
485
                absolute_path
2✔
486
            })
2✔
487
            .collect::<Vec<_>>();
71✔
488

489
        // Start a search from the library root. Courses can be located at any level within the
490
        // library root. However, the course manifests, assets, and its lessons and exercises follow
491
        // a fixed structure.
492
        for entry in WalkDir::new(library_root)
59,440✔
493
            .min_depth(2)
71✔
494
            .into_iter()
71✔
495
            .flatten()
71✔
496
        {
497
            // Ignore any entries which are not directories.
498
            if entry.path().is_dir() {
59,440✔
499
                continue;
14,571✔
500
            }
44,869✔
501

502
            // Ignore any files which are not named `course_manifest.json`.
503
            let file_name = Self::get_file_name(entry.path())?;
44,869✔
504
            if file_name != COURSE_MANIFEST_FILENAME {
44,869✔
505
                continue;
44,389✔
506
            }
480✔
507

480✔
508
            // Ignore any directory that matches the list of paths to ignore.
480✔
509
            if ignored_paths
480✔
510
                .iter()
480✔
511
                .any(|ignored_path| entry.path().starts_with(ignored_path))
480✔
512
            {
513
                continue;
2✔
514
            }
478✔
515

516
            // Open the course manifest and process it.
517
            let mut course_manifest: CourseManifest = Self::open_manifest(entry.path())?;
478✔
518
            course_manifest = course_manifest.normalize_paths(entry.path().parent().unwrap())?;
478✔
519
            library.process_course_manifest(
478✔
520
                entry.path().parent().unwrap(),
478✔
521
                course_manifest,
478✔
522
                &mut index_writer,
478✔
UNCOV
523
            )?;
×
524
        }
525

526
        // Commit the search index writer and initialize the reader in the course library.
527
        index_writer.commit()?;
71✔
528
        library.reader = Some(
71✔
529
            library
71✔
530
                .index
71✔
531
                .reader_builder()
71✔
532
                .reload_policy(ReloadPolicy::OnCommitWithDelay)
71✔
533
                .try_into()?,
71✔
534
        );
535

536
        // Compute the lessons in a course not dependent on any other lesson in the course. This
537
        // allows the scheduler to traverse the lessons in a course in the correct order.
538
        library.unit_graph.write().update_starting_lessons();
71✔
539

71✔
540
        // Perform a check to detect cyclic dependencies to prevent infinite loops during traversal.
71✔
541
        library.unit_graph.read().check_cycles()?;
71✔
542
        Ok(library)
71✔
543
    }
71✔
544

545
    /// Helper function to search the course library.
546
    fn search_helper(&self, query: &str) -> Result<Vec<Ustr>> {
11✔
547
        // Retrieve a searcher from the reader and parse the query.
11✔
548
        if self.reader.is_none() {
11✔
549
            // This should never happen since the reader is initialized in the constructor.
550
            return Ok(Vec::new());
×
551
        }
11✔
552
        let searcher = self.reader.as_ref().unwrap().searcher();
11✔
553
        let id_field = Self::schema_field(ID_SCHEMA_FIELD)?;
11✔
554
        let query_parser = QueryParser::for_index(
11✔
555
            &self.index,
11✔
556
            vec![
11✔
557
                id_field,
11✔
558
                Self::schema_field(NAME_SCHEMA_FIELD)?,
11✔
559
                Self::schema_field(DESCRIPTION_SCHEMA_FIELD)?,
11✔
560
                Self::schema_field(METADATA_SCHEMA_FIELD)?,
11✔
561
            ],
562
        );
563
        let query = query_parser.parse_query(query)?;
11✔
564

565
        // Execute the query and return the results as a list of unit IDs.
566
        let top_docs = searcher.search(&query, &TopDocs::with_limit(50))?;
11✔
567
        top_docs
11✔
568
            .into_iter()
11✔
569
            .map(|(_, doc_address)| {
75✔
570
                let doc: TantivyDocument = searcher.doc(doc_address)?;
75✔
571
                let id = doc.get_first(id_field).unwrap();
75✔
572
                Ok(id.as_str().unwrap_or("").to_string().into())
75✔
573
            })
75✔
574
            .collect::<Result<Vec<Ustr>>>()
11✔
575
    }
11✔
576
}
577

578
impl CourseLibrary for LocalCourseLibrary {
579
    fn get_course_manifest(&self, course_id: Ustr) -> Option<CourseManifest> {
69,297✔
580
        self.course_map.get(&course_id).cloned()
69,297✔
581
    }
69,297✔
582

583
    fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<LessonManifest> {
9,527✔
584
        self.lesson_map.get(&lesson_id).cloned()
9,527✔
585
    }
9,527✔
586

587
    fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<ExerciseManifest> {
81,328✔
588
        self.exercise_map.get(&exercise_id).cloned()
81,328✔
589
    }
81,328✔
590

591
    fn get_course_ids(&self) -> Vec<Ustr> {
1✔
592
        let mut courses = self.course_map.keys().copied().collect::<Vec<Ustr>>();
1✔
593
        courses.sort();
1✔
594
        courses
1✔
595
    }
1✔
596

597
    fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>> {
8✔
598
        let mut lessons = self
8✔
599
            .unit_graph
8✔
600
            .read()
8✔
601
            .get_course_lessons(course_id)?
8✔
602
            .into_iter()
7✔
603
            .collect::<Vec<Ustr>>();
7✔
604
        lessons.sort();
7✔
605
        Some(lessons)
7✔
606
    }
8✔
607

608
    fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>> {
18✔
609
        let mut exercises = self
18✔
610
            .unit_graph
18✔
611
            .read()
18✔
612
            .get_lesson_exercises(lesson_id)?
18✔
613
            .into_iter()
17✔
614
            .collect::<Vec<Ustr>>();
17✔
615
        exercises.sort();
17✔
616
        Some(exercises)
17✔
617
    }
18✔
618

619
    fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr> {
18✔
620
        let mut exercises = match unit_id {
18✔
621
            Some(unit_id) => {
4✔
622
                // Return the exercises according to the type of the unit.
4✔
623
                let unit_type = self.unit_graph.read().get_unit_type(unit_id);
4✔
624
                match unit_type {
3✔
625
                    Some(UnitType::Course) => self
1✔
626
                        .unit_graph
1✔
627
                        .read()
1✔
628
                        .get_course_lessons(unit_id)
1✔
629
                        .unwrap_or_default()
1✔
630
                        .into_iter()
1✔
631
                        .flat_map(|lesson_id| {
2✔
632
                            self.unit_graph
2✔
633
                                .read()
2✔
634
                                .get_lesson_exercises(lesson_id)
2✔
635
                                .unwrap_or_default()
2✔
636
                        })
2✔
637
                        .collect::<Vec<Ustr>>(),
1✔
638
                    Some(UnitType::Lesson) => self
1✔
639
                        .unit_graph
1✔
640
                        .read()
1✔
641
                        .get_lesson_exercises(unit_id)
1✔
642
                        .unwrap_or_default()
1✔
643
                        .into_iter()
1✔
644
                        .collect::<Vec<Ustr>>(),
1✔
645
                    Some(UnitType::Exercise) => vec![unit_id],
1✔
646
                    None => vec![],
1✔
647
                }
648
            }
649
            // If none, return all the exercises in the library.
650
            None => self.exercise_map.keys().copied().collect::<Vec<Ustr>>(),
14✔
651
        };
652

653
        // Sort the exercises before returning them.
654
        exercises.sort();
18✔
655
        exercises
18✔
656
    }
18✔
657

658
    fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet {
4✔
659
        match unit_type {
3✔
660
            Some(UnitType::Course) => self
1✔
661
                .course_map
1✔
662
                .iter()
1✔
663
                .filter_map(|(id, _)| {
8✔
664
                    if id.starts_with(prefix) {
8✔
665
                        Some(*id)
1✔
666
                    } else {
667
                        None
7✔
668
                    }
669
                })
8✔
670
                .collect(),
1✔
671
            Some(UnitType::Lesson) => self
1✔
672
                .lesson_map
1✔
673
                .iter()
1✔
674
                .filter_map(|(id, _)| {
18✔
675
                    if id.starts_with(prefix) {
18✔
676
                        Some(*id)
1✔
677
                    } else {
678
                        None
17✔
679
                    }
680
                })
18✔
681
                .collect(),
1✔
682
            Some(UnitType::Exercise) => self
1✔
683
                .exercise_map
1✔
684
                .iter()
1✔
685
                .filter_map(|(id, _)| {
170✔
686
                    if id.starts_with(prefix) {
170✔
687
                        Some(*id)
1✔
688
                    } else {
689
                        None
169✔
690
                    }
691
                })
170✔
692
                .collect(),
1✔
693
            None => self
1✔
694
                .course_map
1✔
695
                .keys()
1✔
696
                .chain(self.lesson_map.keys())
1✔
697
                .chain(self.exercise_map.keys())
1✔
698
                .filter(|id| id.starts_with(prefix))
196✔
699
                .copied()
1✔
700
                .collect(),
1✔
701
        }
702
    }
4✔
703

704
    fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError> {
11✔
705
        self.search_helper(query)
11✔
706
            .map_err(|e| CourseLibraryError::Search(query.into(), e))
11✔
707
    }
11✔
708
}
709

710
impl GetUnitGraph for LocalCourseLibrary {
711
    fn get_unit_graph(&self) -> Arc<RwLock<InMemoryUnitGraph>> {
71✔
712
        self.unit_graph.clone()
71✔
713
    }
71✔
714
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc