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

trane-project / trane / 12735888711

12 Jan 2025 06:06PM UTC coverage: 92.973% (-6.9%) from 99.866%
12735888711

Pull #319

github

web-flow
Merge 2bba403ef into f4478d7eb
Pull Request #319: Update to rust 1.84

69 of 77 new or added lines in 18 files covered. (89.61%)

236 existing lines in 13 files now uncovered.

9672 of 10403 relevant lines covered (92.97%)

87046.61 hits per line

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

98.07
/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::{anyhow, ensure, Context, Result};
8
use parking_lot::RwLock;
9
use serde::de::DeserializeOwned;
10
use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path, sync::Arc};
11
use tantivy::{
12
    collector::TopDocs,
13
    doc,
14
    query::QueryParser,
15
    schema::{Field, Schema, Value, STORED, TEXT},
16
    Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument,
17
};
18
use ustr::{Ustr, UstrMap, UstrSet};
19
use walkdir::WalkDir;
20

21
use crate::{
22
    data::{
23
        CourseManifest, ExerciseManifest, GenerateManifests, LessonManifest, NormalizePaths,
24
        UnitType, UserPreferences,
25
    },
26
    error::CourseLibraryError,
27
    graph::{InMemoryUnitGraph, UnitGraph},
28
};
29

30
/// The file name for all course manifests.
31
pub const COURSE_MANIFEST_FILENAME: &str = "course_manifest.json";
32

33
/// The file name for all lesson manifests.
34
pub const LESSON_MANIFEST_FILENAME: &str = "lesson_manifest.json";
35

36
/// The file name for all exercise manifests.
37
pub const EXERCISE_MANIFEST_FILENAME: &str = "exercise_manifest.json";
38

39
/// The name of the field for the unit ID in the search schema.
40
const ID_SCHEMA_FIELD: &str = "id";
41

42
/// The name of the field for the unit name in the search schema.
43
const NAME_SCHEMA_FIELD: &str = "name";
44

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

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

51
/// A trait that manages a course library, its corresponding manifest files, and provides basic
52
/// operations to retrieve the courses, lessons in a course, and exercises in a lesson.
53
pub trait CourseLibrary {
54
    /// Returns the course manifest for the given course.
55
    fn get_course_manifest(&self, course_id: Ustr) -> Option<CourseManifest>;
56

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

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

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

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

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

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

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

79
    /// Returns the IDs of all the units which match the given query.
80
    fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError>;
81
}
82

83
/// A trait that retrieves the unit graph generated after reading a course library.
84
pub(crate) trait GetUnitGraph {
85
    /// Returns a reference to the in-memory unit graph describing the dependencies among the
86
    /// courses and lessons in this library.
87
    fn get_unit_graph(&self) -> Arc<RwLock<InMemoryUnitGraph>>;
88
}
89

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

116
    /// A mapping of course ID to its corresponding course manifest.
117
    course_map: UstrMap<CourseManifest>,
118

119
    /// A mapping of lesson ID to its corresponding lesson manifest.
120
    lesson_map: UstrMap<LessonManifest>,
121

122
    /// A mapping of exercise ID to its corresponding exercise manifest.
123
    exercise_map: UstrMap<ExerciseManifest>,
124

125
    /// The user preferences.
126
    user_preferences: UserPreferences,
127

128
    /// A tantivy index used for searching the course library.
129
    index: Index,
130

131
    /// A reader to access the search index.
132
    reader: Option<IndexReader>,
133
}
134

135
impl LocalCourseLibrary {
136
    /// Returns the tantivy schema used for searching the course library.
137
    fn search_schema() -> Schema {
61,055✔
138
        let mut schema = Schema::builder();
61,055✔
139
        schema.add_text_field(ID_SCHEMA_FIELD, TEXT | STORED);
61,055✔
140
        schema.add_text_field(NAME_SCHEMA_FIELD, TEXT | STORED);
61,055✔
141
        schema.add_text_field(DESCRIPTION_SCHEMA_FIELD, TEXT | STORED);
61,055✔
142
        schema.add_text_field(METADATA_SCHEMA_FIELD, TEXT | STORED);
61,055✔
143
        schema.build()
61,055✔
144
    }
61,055✔
145

146
    /// Returns the field in the search schema with the given name.
147
    fn schema_field(field_name: &str) -> Result<Field> {
60,984✔
148
        let schema = Self::search_schema();
60,984✔
149
        let field = schema.get_field(field_name)?;
60,984✔
150
        Ok(field)
60,984✔
151
    }
60,984✔
152

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

165
        // Declare the base document with the ID, name, and description fields.
166
        let mut doc = doc!(
15,235✔
167
            Self::schema_field(ID_SCHEMA_FIELD)? => id.to_string(),
15,235✔
168
            Self::schema_field(NAME_SCHEMA_FIELD)? => name.to_string(),
15,235✔
169
            Self::schema_field(DESCRIPTION_SCHEMA_FIELD)? => description.to_string(),
15,235✔
170
        );
171

172
        // Add the metadata. Encode each key-value pair as a string in the format "key:value". Then
173
        // add the document to the index.
174
        let metadata_field = Self::schema_field(METADATA_SCHEMA_FIELD)?;
15,235✔
175
        if let Some(metadata) = metadata {
15,235✔
176
            for (key, values) in metadata {
2,384✔
177
                for value in values {
996✔
178
                    doc.add_text(metadata_field, format!("{key}:{value}"));
498✔
179
                }
498✔
180
            }
181
        }
13,349✔
182
        index_writer.add_document(doc)?;
15,235✔
183
        Ok(())
15,235✔
184
    }
15,235✔
185

186
    /// Opens the course, lesson, or exercise manifest located at the given path.
187
    fn open_manifest<T: DeserializeOwned>(path: &Path) -> Result<T> {
14,463✔
188
        let display = path.display();
14,463✔
189
        let file = File::open(path).context(format!("cannot open manifest file {display}"))?;
14,463✔
190
        let reader = BufReader::new(file);
14,463✔
191
        serde_json::from_reader(reader).context(format!("cannot parse manifest file {display}"))
14,463✔
192
    }
14,463✔
193

194
    /// Returns the file name of the given path.
195
    fn get_file_name(path: &Path) -> Result<String> {
87,886✔
196
        Ok(path
87,886✔
197
            .file_name()
87,886✔
198
            .ok_or(anyhow!("cannot get file name from DirEntry"))?
87,886✔
199
            .to_str()
87,886✔
200
            .ok_or(anyhow!("invalid dir entry {}", path.display()))?
87,886✔
201
            .to_string())
87,886✔
202
    }
87,886✔
203

204
    /// Adds an exercise to the course library given its manifest and the manifest of the lesson to
205
    /// which it belongs.
206
    fn process_exercise_manifest(
13,277✔
207
        &mut self,
13,277✔
208
        lesson_manifest: &LessonManifest,
13,277✔
209
        exercise_manifest: ExerciseManifest,
13,277✔
210
        index_writer: &mut IndexWriter,
13,277✔
211
    ) -> Result<()> {
13,277✔
212
        // Verify that the IDs mentioned in the manifests are valid and agree with each other.
13,277✔
213
        ensure!(!exercise_manifest.id.is_empty(), "ID in manifest is empty",);
13,277✔
214
        ensure!(
13,277✔
215
            exercise_manifest.lesson_id == lesson_manifest.id,
13,277✔
UNCOV
216
            "lesson_id in manifest for exercise {} does not match the manifest for lesson {}",
×
217
            exercise_manifest.id,
218
            lesson_manifest.id,
219
        );
220
        ensure!(
13,277✔
221
            exercise_manifest.course_id == lesson_manifest.course_id,
13,277✔
UNCOV
222
            "course_id in manifest for exercise {} does not match the manifest for course {}",
×
223
            exercise_manifest.id,
224
            lesson_manifest.course_id,
225
        );
226

227
        // Add the exercise manifest to the search index.
228
        Self::add_to_index_writer(
13,277✔
229
            index_writer,
13,277✔
230
            exercise_manifest.id,
13,277✔
231
            &exercise_manifest.name,
13,277✔
232
            exercise_manifest.description.as_deref(),
13,277✔
233
            None,
13,277✔
234
        )?;
13,277✔
235

236
        // Add the exercise to the unit graph and exercise map.
237
        self.unit_graph
13,277✔
238
            .write()
13,277✔
239
            .add_exercise(exercise_manifest.id, exercise_manifest.lesson_id)?;
13,277✔
240
        self.exercise_map
13,277✔
241
            .insert(exercise_manifest.id, exercise_manifest);
13,277✔
242
        Ok(())
13,277✔
243
    }
13,277✔
244

245
    /// Adds a lesson to the course library given its manifest and the manifest of the course to
246
    /// which it belongs. It also traverses the given `DirEntry` and adds all the exercises in the
247
    /// lesson.
248
    fn process_lesson_manifest(
1,480✔
249
        &mut self,
1,480✔
250
        lesson_root: &Path,
1,480✔
251
        course_manifest: &CourseManifest,
1,480✔
252
        lesson_manifest: &LessonManifest,
1,480✔
253
        index_writer: &mut IndexWriter,
1,480✔
254
        generated_exercises: Option<&Vec<ExerciseManifest>>,
1,480✔
255
    ) -> Result<()> {
1,480✔
256
        // Verify that the IDs mentioned in the manifests are valid and agree with each other.
1,480✔
257
        ensure!(!lesson_manifest.id.is_empty(), "ID in manifest is empty",);
1,480✔
258
        ensure!(
1,480✔
259
            lesson_manifest.course_id == course_manifest.id,
1,480✔
UNCOV
260
            "course_id in manifest for lesson {} does not match the manifest for course {}",
×
261
            lesson_manifest.id,
262
            course_manifest.id,
263
        );
264

265
        // Add the lesson, the dependencies, and the superseded units explicitly listed in the
266
        // lesson manifest.
267
        self.unit_graph
1,480✔
268
            .write()
1,480✔
269
            .add_lesson(lesson_manifest.id, lesson_manifest.course_id)?;
1,480✔
270
        self.unit_graph.write().add_dependencies(
1,480✔
271
            lesson_manifest.id,
1,480✔
272
            UnitType::Lesson,
1,480✔
273
            &lesson_manifest.dependencies,
1,480✔
274
        )?;
1,480✔
275
        self.unit_graph
1,480✔
276
            .write()
1,480✔
277
            .add_superseded(lesson_manifest.id, &lesson_manifest.superseded);
1,480✔
278

279
        // Add the generated exercises to the lesson.
280
        if let Some(exercises) = generated_exercises {
1,480✔
281
            for exercise_manifest in exercises {
772✔
282
                let exercise_manifest = &exercise_manifest.normalize_paths(lesson_root)?;
641✔
283
                self.process_exercise_manifest(
641✔
284
                    lesson_manifest,
641✔
285
                    exercise_manifest.clone(),
641✔
286
                    index_writer,
641✔
287
                )?;
641✔
288
            }
289
        }
1,349✔
290

291
        // Start a new search from the parent of the passed `DirEntry`, which corresponds to the
292
        // lesson's root. Each exercise in the lesson must be contained in a directory that is a
293
        // direct descendant of its root. Therefore, all the exercise manifests will be found at a
294
        // depth of two.
295
        for entry in WalkDir::new(lesson_root).min_depth(2).max_depth(2) {
39,856✔
296
            match entry {
39,856✔
UNCOV
297
                Err(_) => continue,
×
298
                Ok(exercise_dir_entry) => {
39,856✔
299
                    // Ignore any entries which are not directories.
39,856✔
300
                    if exercise_dir_entry.path().is_dir() {
39,856✔
UNCOV
301
                        continue;
×
302
                    }
39,856✔
303

304
                    // Ignore any files which are not named `exercise_manifest.json`.
305
                    let file_name = Self::get_file_name(exercise_dir_entry.path())?;
39,856✔
306
                    if file_name != EXERCISE_MANIFEST_FILENAME {
39,856✔
307
                        continue;
27,220✔
308
                    }
12,636✔
309

310
                    // Open the exercise manifest and process it.
311
                    let mut exercise_manifest: ExerciseManifest =
12,636✔
312
                        Self::open_manifest(exercise_dir_entry.path())?;
12,636✔
313
                    exercise_manifest = exercise_manifest
12,636✔
314
                        .normalize_paths(exercise_dir_entry.path().parent().unwrap())?;
12,636✔
315
                    self.process_exercise_manifest(
12,636✔
316
                        lesson_manifest,
12,636✔
317
                        exercise_manifest,
12,636✔
318
                        index_writer,
12,636✔
319
                    )?;
12,636✔
320
                }
321
            }
322
        }
323

324
        // Add the lesson manifest to the lesson map and the search index.
325
        self.lesson_map
1,480✔
326
            .insert(lesson_manifest.id, lesson_manifest.clone());
1,480✔
327
        Self::add_to_index_writer(
1,480✔
328
            index_writer,
1,480✔
329
            lesson_manifest.id,
1,480✔
330
            &lesson_manifest.name,
1,480✔
331
            lesson_manifest.description.as_deref(),
1,480✔
332
            lesson_manifest.metadata.as_ref(),
1,480✔
333
        )?;
1,480✔
334
        Ok(())
1,480✔
335
    }
1,480✔
336

337
    /// Adds a course to the course library given its manifest. It also traverses the given
338
    /// `DirEntry` and adds all the lessons in the course.
339
    fn process_course_manifest(
478✔
340
        &mut self,
478✔
341
        course_root: &Path,
478✔
342
        mut course_manifest: CourseManifest,
478✔
343
        index_writer: &mut IndexWriter,
478✔
344
    ) -> Result<()> {
478✔
345
        ensure!(!course_manifest.id.is_empty(), "ID in manifest is empty",);
478✔
346

347
        // Add the course, the dependencies, and the superseded units explicitly listed in the
348
        // manifest.
349
        self.unit_graph.write().add_course(course_manifest.id)?;
478✔
350
        self.unit_graph.write().add_dependencies(
478✔
351
            course_manifest.id,
478✔
352
            UnitType::Course,
478✔
353
            &course_manifest.dependencies,
478✔
354
        )?;
478✔
355
        self.unit_graph
478✔
356
            .write()
478✔
357
            .add_superseded(course_manifest.id, &course_manifest.superseded);
478✔
358

359
        // If the course has a generator config, generate the lessons and exercises and add them to
360
        // the library.
361
        if let Some(generator_config) = &course_manifest.generator_config {
478✔
362
            let generated_course = generator_config.generate_manifests(
19✔
363
                course_root,
19✔
364
                &course_manifest,
19✔
365
                &self.user_preferences,
19✔
366
            )?;
19✔
367
            for (lesson_manifest, exercise_manifests) in generated_course.lessons {
150✔
368
                // All the generated lessons will use the root of the course as their root.
369
                self.process_lesson_manifest(
131✔
370
                    course_root,
131✔
371
                    &course_manifest,
131✔
372
                    &lesson_manifest,
131✔
373
                    index_writer,
131✔
374
                    Some(&exercise_manifests),
131✔
375
                )?;
131✔
376
            }
377

378
            // Update the course manifest's metadata, material, and instructions if needed.
379
            if generated_course.updated_metadata.is_some() {
19✔
380
                course_manifest.metadata = generated_course.updated_metadata;
12✔
381
            }
12✔
382
            if generated_course.updated_instructions.is_some() {
19✔
383
                course_manifest.course_instructions = generated_course.updated_instructions;
12✔
384
            }
12✔
385
        }
459✔
386

387
        // Start a new search from the parent of the passed `DirEntry`, which corresponds to the
388
        // course's root. Each lesson in the course must be contained in a directory that is a
389
        // direct descendant of its root. Therefore, all the lesson manifests will be found at a
390
        // depth of two.
391
        for entry in WalkDir::new(course_root).min_depth(2).max_depth(2) {
16,894✔
392
            match entry {
16,894✔
UNCOV
393
                Err(_) => continue,
×
394
                Ok(lesson_dir_entry) => {
16,894✔
395
                    // Ignore any entries which are not directories.
16,894✔
396
                    if lesson_dir_entry.path().is_dir() {
16,894✔
397
                        continue;
12,636✔
398
                    }
4,258✔
399

400
                    // Ignore any files which are not named `lesson_manifest.json`.
401
                    let file_name = Self::get_file_name(lesson_dir_entry.path())?;
4,258✔
402
                    if file_name != LESSON_MANIFEST_FILENAME {
4,258✔
403
                        continue;
2,909✔
404
                    }
1,349✔
405

406
                    // Open the lesson manifest and process it.
407
                    let mut lesson_manifest: LessonManifest =
1,349✔
408
                        Self::open_manifest(lesson_dir_entry.path())?;
1,349✔
409
                    lesson_manifest = lesson_manifest
1,349✔
410
                        .normalize_paths(lesson_dir_entry.path().parent().unwrap())?;
1,349✔
411
                    self.process_lesson_manifest(
1,349✔
412
                        lesson_dir_entry.path().parent().unwrap(),
1,349✔
413
                        &course_manifest,
1,349✔
414
                        &lesson_manifest,
1,349✔
415
                        index_writer,
1,349✔
416
                        None,
1,349✔
417
                    )?;
1,349✔
418
                }
419
            }
420
        }
421

422
        // Add the course manifest to the course map and the search index. This needs to happen at
423
        // the end in case the course has a generator config and the course manifest was updated.
424
        self.course_map
478✔
425
            .insert(course_manifest.id, course_manifest.clone());
478✔
426
        Self::add_to_index_writer(
478✔
427
            index_writer,
478✔
428
            course_manifest.id,
478✔
429
            &course_manifest.name,
478✔
430
            course_manifest.description.as_deref(),
478✔
431
            course_manifest.metadata.as_ref(),
478✔
432
        )?;
478✔
433
        Ok(())
478✔
434
    }
478✔
435

436
    /// A constructor taking the path to the root of the library.
437
    pub fn new(library_root: &Path, user_preferences: UserPreferences) -> Result<Self> {
71✔
438
        let mut library = LocalCourseLibrary {
71✔
439
            course_map: UstrMap::default(),
71✔
440
            lesson_map: UstrMap::default(),
71✔
441
            exercise_map: UstrMap::default(),
71✔
442
            user_preferences,
71✔
443
            unit_graph: Arc::new(RwLock::new(InMemoryUnitGraph::default())),
71✔
444
            index: Index::create_in_ram(Self::search_schema()),
71✔
445
            reader: None,
71✔
446
        };
71✔
447

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

451
        // Convert the list of paths to ignore into absolute paths.
452
        let absolute_root = library_root.canonicalize()?;
71✔
453
        let ignored_paths = library
71✔
454
            .user_preferences
71✔
455
            .ignored_paths
71✔
456
            .iter()
71✔
457
            .map(|path| {
71✔
458
                let mut absolute_path = absolute_root.clone();
2✔
459
                absolute_path.push(path);
2✔
460
                absolute_path
2✔
461
            })
71✔
462
            .collect::<Vec<_>>();
71✔
463

464
        // Start a search from the library root. Courses can be located at any level within the
465
        // library root. However, the course manifests, assets, and its lessons and exercises follow
466
        // a fixed structure.
467
        for entry in WalkDir::new(library_root).min_depth(2) {
57,975✔
468
            match entry {
57,975✔
UNCOV
469
                Err(_) => continue,
×
470
                Ok(dir_entry) => {
57,975✔
471
                    // Ignore any entries which are not directories.
57,975✔
472
                    if dir_entry.path().is_dir() {
57,975✔
473
                        continue;
14,203✔
474
                    }
43,772✔
475

476
                    // Ignore any files which are not named `course_manifest.json`.
477
                    let file_name = Self::get_file_name(dir_entry.path())?;
43,772✔
478
                    if file_name != COURSE_MANIFEST_FILENAME {
43,772✔
479
                        continue;
43,292✔
480
                    }
480✔
481

480✔
482
                    // Ignore any directory that matches the list of paths to ignore.
480✔
483
                    if ignored_paths
480✔
484
                        .iter()
480✔
485
                        .any(|ignored_path| dir_entry.path().starts_with(ignored_path))
480✔
486
                    {
487
                        continue;
2✔
488
                    }
478✔
489

490
                    // Open the course manifest and process it.
491
                    let mut course_manifest: CourseManifest =
478✔
492
                        Self::open_manifest(dir_entry.path())?;
478✔
493
                    course_manifest =
478✔
494
                        course_manifest.normalize_paths(dir_entry.path().parent().unwrap())?;
478✔
495
                    library.process_course_manifest(
478✔
496
                        dir_entry.path().parent().unwrap(),
478✔
497
                        course_manifest,
478✔
498
                        &mut index_writer,
478✔
499
                    )?;
478✔
500
                }
501
            }
502
        }
503

504
        // Commit the search index writer and initialize the reader in the course library.
505
        index_writer.commit()?;
71✔
506
        library.reader = Some(
71✔
507
            library
71✔
508
                .index
71✔
509
                .reader_builder()
71✔
510
                .reload_policy(ReloadPolicy::OnCommitWithDelay)
71✔
511
                .try_into()?,
71✔
512
        );
513

514
        // Compute the lessons in a course not dependent on any other lesson in the course. This
515
        // allows the scheduler to traverse the lessons in a course in the correct order.
516
        library.unit_graph.write().update_starting_lessons();
71✔
517

71✔
518
        // Perform a check to detect cyclic dependencies to prevent infinite loops during traversal.
71✔
519
        library.unit_graph.read().check_cycles()?;
71✔
520
        Ok(library)
71✔
521
    }
71✔
522

523
    /// Helper function to search the course library.
524
    fn search_helper(&self, query: &str) -> Result<Vec<Ustr>> {
11✔
525
        // Retrieve a searcher from the reader and parse the query.
11✔
526
        if self.reader.is_none() {
11✔
527
            // This should never happen since the reader is initialized in the constructor.
NEW
528
            return Ok(Vec::new());
×
529
        }
11✔
530
        let searcher = self.reader.as_ref().unwrap().searcher();
11✔
531
        let id_field = Self::schema_field(ID_SCHEMA_FIELD)?;
11✔
532
        let query_parser = QueryParser::for_index(
11✔
533
            &self.index,
11✔
534
            vec![
11✔
535
                id_field,
11✔
536
                Self::schema_field(NAME_SCHEMA_FIELD)?,
11✔
537
                Self::schema_field(DESCRIPTION_SCHEMA_FIELD)?,
11✔
538
                Self::schema_field(METADATA_SCHEMA_FIELD)?,
11✔
539
            ],
540
        );
541
        let query = query_parser.parse_query(query)?;
11✔
542

543
        // Execute the query and return the results as a list of unit IDs.
544
        let top_docs = searcher.search(&query, &TopDocs::with_limit(50))?;
11✔
545
        top_docs
11✔
546
            .into_iter()
11✔
547
            .map(|(_, doc_address)| {
75✔
548
                let doc: TantivyDocument = searcher.doc(doc_address)?;
75✔
549
                let id = doc.get_first(id_field).unwrap();
75✔
550
                Ok(id.as_str().unwrap_or("").to_string().into())
75✔
551
            })
75✔
552
            .collect::<Result<Vec<Ustr>>>()
11✔
553
    }
11✔
554
}
555

556
impl CourseLibrary for LocalCourseLibrary {
557
    fn get_course_manifest(&self, course_id: Ustr) -> Option<CourseManifest> {
45,464✔
558
        self.course_map.get(&course_id).cloned()
45,464✔
559
    }
45,464✔
560

561
    fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<LessonManifest> {
9,473✔
562
        self.lesson_map.get(&lesson_id).cloned()
9,473✔
563
    }
9,473✔
564

565
    fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<ExerciseManifest> {
79,639✔
566
        self.exercise_map.get(&exercise_id).cloned()
79,639✔
567
    }
79,639✔
568

569
    fn get_course_ids(&self) -> Vec<Ustr> {
1✔
570
        let mut courses = self.course_map.keys().copied().collect::<Vec<Ustr>>();
1✔
571
        courses.sort();
1✔
572
        courses
1✔
573
    }
1✔
574

575
    fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>> {
8✔
576
        let mut lessons = self
8✔
577
            .unit_graph
8✔
578
            .read()
8✔
579
            .get_course_lessons(course_id)?
8✔
580
            .into_iter()
7✔
581
            .collect::<Vec<Ustr>>();
7✔
582
        lessons.sort();
7✔
583
        Some(lessons)
7✔
584
    }
8✔
585

586
    fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>> {
18✔
587
        let mut exercises = self
18✔
588
            .unit_graph
18✔
589
            .read()
18✔
590
            .get_lesson_exercises(lesson_id)?
18✔
591
            .into_iter()
17✔
592
            .collect::<Vec<Ustr>>();
17✔
593
        exercises.sort();
17✔
594
        Some(exercises)
17✔
595
    }
18✔
596

597
    fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr> {
18✔
598
        let mut exercises = match unit_id {
18✔
599
            Some(unit_id) => {
4✔
600
                // Return the exercises according to the type of the unit.
4✔
601
                let unit_type = self.unit_graph.read().get_unit_type(unit_id);
4✔
602
                match unit_type {
3✔
603
                    Some(UnitType::Course) => self
1✔
604
                        .unit_graph
1✔
605
                        .read()
1✔
606
                        .get_course_lessons(unit_id)
1✔
607
                        .unwrap_or_default()
1✔
608
                        .into_iter()
1✔
609
                        .flat_map(|lesson_id| {
2✔
610
                            self.unit_graph
2✔
611
                                .read()
2✔
612
                                .get_lesson_exercises(lesson_id)
2✔
613
                                .unwrap_or_default()
2✔
614
                        })
2✔
615
                        .collect::<Vec<Ustr>>(),
1✔
616
                    Some(UnitType::Lesson) => self
1✔
617
                        .unit_graph
1✔
618
                        .read()
1✔
619
                        .get_lesson_exercises(unit_id)
1✔
620
                        .unwrap_or_default()
1✔
621
                        .into_iter()
1✔
622
                        .collect::<Vec<Ustr>>(),
1✔
623
                    Some(UnitType::Exercise) => vec![unit_id],
1✔
624
                    None => vec![],
1✔
625
                }
626
            }
627
            // If none, return all the exercises in the library.
628
            None => self.exercise_map.keys().copied().collect::<Vec<Ustr>>(),
14✔
629
        };
630

631
        // Sort the exercises before returning them.
632
        exercises.sort();
18✔
633
        exercises
18✔
634
    }
18✔
635

636
    fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet {
4✔
637
        match unit_type {
3✔
638
            Some(UnitType::Course) => self
1✔
639
                .course_map
1✔
640
                .iter()
1✔
641
                .filter_map(|(id, _)| {
8✔
642
                    if id.starts_with(prefix) {
8✔
643
                        Some(*id)
1✔
644
                    } else {
645
                        None
7✔
646
                    }
647
                })
8✔
648
                .collect(),
1✔
649
            Some(UnitType::Lesson) => self
1✔
650
                .lesson_map
1✔
651
                .iter()
1✔
652
                .filter_map(|(id, _)| {
18✔
653
                    if id.starts_with(prefix) {
18✔
654
                        Some(*id)
1✔
655
                    } else {
656
                        None
17✔
657
                    }
658
                })
18✔
659
                .collect(),
1✔
660
            Some(UnitType::Exercise) => self
1✔
661
                .exercise_map
1✔
662
                .iter()
1✔
663
                .filter_map(|(id, _)| {
170✔
664
                    if id.starts_with(prefix) {
170✔
665
                        Some(*id)
1✔
666
                    } else {
667
                        None
169✔
668
                    }
669
                })
170✔
670
                .collect(),
1✔
671
            None => self
1✔
672
                .course_map
1✔
673
                .keys()
1✔
674
                .chain(self.lesson_map.keys())
1✔
675
                .chain(self.exercise_map.keys())
1✔
676
                .filter(|id| id.starts_with(prefix))
196✔
677
                .copied()
1✔
678
                .collect(),
1✔
679
        }
680
    }
4✔
681

682
    fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError> {
11✔
683
        self.search_helper(query)
11✔
684
            .map_err(|e| CourseLibraryError::Search(query.into(), e))
11✔
685
    }
11✔
686
}
687

688
impl GetUnitGraph for LocalCourseLibrary {
689
    fn get_unit_graph(&self) -> Arc<RwLock<InMemoryUnitGraph>> {
71✔
690
        self.unit_graph.clone()
71✔
691
    }
71✔
692
}
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