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

trane-project / trane / 4099037897

pending completion
4099037897

push

github

GitHub
Normalize course instructions file (#177)

4975 of 4975 relevant lines covered (100.0%)

92589.71 hits per line

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

100.0
/src/course_builder/knowledge_base_builder.rs
1
//! Contains utilities to make it easier to build knowledge base courses.
2
//!
3
//! The knowledge base course format is a plain-text format that is intended to be easy to edit by
4
//! hand. This module contains utilities to make it easier to generate these files, specially for
5
//! testing purposes.
6

7
use anyhow::{ensure, Result};
8
use std::{
9
    fs::{self, create_dir_all, File},
10
    io::Write,
11
    path::Path,
12
};
13

14
use crate::{
15
    course_builder::AssetBuilder,
16
    data::{course_generator::knowledge_base::*, CourseManifest},
17
};
18

19
/// A builder to generate a knowledge base exercise and associated assets.
20
pub struct ExerciseBuilder {
21
    /// The knowledge base exercise to build.
22
    pub exercise: KnowledgeBaseExercise,
23

24
    /// The assets associated with this exercise, which include the front and back of the flashcard.
25
    pub asset_builders: Vec<AssetBuilder>,
26
}
27

28
impl ExerciseBuilder {
29
    /// Writes the files needed for this exercise to the given directory.
30
    pub fn build(&self, lesson_directory: &Path) -> Result<()> {
101✔
31
        // Build all the assets.
32
        for builder in &self.asset_builders {
263✔
33
            builder.build(lesson_directory)?;
162✔
34
        }
35

36
        // Write the exercise properties to the corresponding file.
37
        if let Some(name) = &self.exercise.name {
101✔
38
            let name_json = serde_json::to_string_pretty(name)?;
1✔
39
            let name_path = lesson_directory.join(format!(
1✔
40
                "{}{}",
41
                self.exercise.short_id, EXERCISE_NAME_SUFFIX
42
            ));
43
            let mut name_file = File::create(name_path)?;
1✔
44
            name_file.write_all(name_json.as_bytes())?;
1✔
45
        }
1✔
46
        if let Some(description) = &self.exercise.description {
101✔
47
            let description_json = serde_json::to_string_pretty(description)?;
1✔
48
            let description_path = lesson_directory.join(format!(
1✔
49
                "{}{}",
50
                self.exercise.short_id, EXERCISE_DESCRIPTION_SUFFIX
51
            ));
52
            let mut description_file = File::create(description_path)?;
1✔
53
            description_file.write_all(description_json.as_bytes())?;
1✔
54
        }
1✔
55
        if let Some(exercise_type) = &self.exercise.exercise_type {
101✔
56
            let exercise_type_json = serde_json::to_string_pretty(exercise_type)?;
1✔
57
            let exercise_type_path = lesson_directory.join(format!(
1✔
58
                "{}{}",
59
                self.exercise.short_id, EXERCISE_TYPE_SUFFIX
60
            ));
61
            let mut exercise_type_file = File::create(exercise_type_path)?;
1✔
62
            exercise_type_file.write_all(exercise_type_json.as_bytes())?;
1✔
63
        }
1✔
64
        Ok(())
101✔
65
    }
101✔
66
}
67

68
/// A builder to generate a knowledge base lesson and associated assets.
69
pub struct LessonBuilder {
70
    /// The knowledge base lesson to build.
71
    pub lesson: KnowledgeBaseLesson,
72

73
    /// The exercise builders for this lesson.
74
    pub exercises: Vec<ExerciseBuilder>,
75

76
    /// The assets associated with this lesson, which include the lesson instructions and materials.
77
    pub assets: Vec<AssetBuilder>,
78
}
79

80
impl LessonBuilder {
81
    /// Writes the files needed for this lesson to the given directory.
82
    pub fn build(&self, lesson_directory: &Path) -> Result<()> {
21✔
83
        // Build all the assets.
84
        for builder in &self.assets {
23✔
85
            builder.build(lesson_directory)?;
2✔
86
        }
87

88
        // Build all the exercises.
89
        for builder in &self.exercises {
122✔
90
            builder.build(lesson_directory)?;
101✔
91
        }
92

93
        // Write the lesson properties to the corresponding file.
94
        if let Some(name) = &self.lesson.name {
21✔
95
            let name_json = serde_json::to_string_pretty(name)?;
1✔
96
            let name_path = lesson_directory.join(LESSON_NAME_FILE);
1✔
97
            let mut name_file = File::create(name_path)?;
1✔
98
            name_file.write_all(name_json.as_bytes())?;
1✔
99
        }
1✔
100
        if let Some(description) = &self.lesson.description {
21✔
101
            let description_json = serde_json::to_string_pretty(description)?;
1✔
102
            let description_path = lesson_directory.join(LESSON_DESCRIPTION_FILE);
1✔
103
            let mut description_file = File::create(description_path)?;
1✔
104
            description_file.write_all(description_json.as_bytes())?;
1✔
105
        }
1✔
106
        if let Some(dependencies) = &self.lesson.dependencies {
21✔
107
            let dependencies_json = serde_json::to_string_pretty(dependencies)?;
21✔
108
            let dependencies_path = lesson_directory.join(LESSON_DEPENDENCIES_FILE);
21✔
109
            let mut dependencies_file = File::create(dependencies_path)?;
21✔
110
            dependencies_file.write_all(dependencies_json.as_bytes())?;
21✔
111
        }
21✔
112
        if let Some(metadata) = &self.lesson.metadata {
21✔
113
            let metadata_json = serde_json::to_string_pretty(metadata)?;
1✔
114
            let metadata_path = lesson_directory.join(LESSON_METADATA_FILE);
1✔
115
            let mut metadata_file = File::create(metadata_path)?;
1✔
116
            metadata_file.write_all(metadata_json.as_bytes())?;
1✔
117
        }
1✔
118
        if let Some(instructions) = &self.lesson.instructions {
21✔
119
            let instructions_json = serde_json::to_string_pretty(instructions)?;
1✔
120
            let instructions_path = lesson_directory.join(LESSON_INSTRUCTIONS_FILE);
1✔
121
            let mut instructions_file = File::create(instructions_path)?;
1✔
122
            instructions_file.write_all(instructions_json.as_bytes())?;
1✔
123
        }
1✔
124
        if let Some(material) = &self.lesson.material {
21✔
125
            let material_json = serde_json::to_string_pretty(material)?;
1✔
126
            let material_path = lesson_directory.join(LESSON_MATERIAL_FILE);
1✔
127
            let mut material_file = File::create(material_path)?;
1✔
128
            material_file.write_all(material_json.as_bytes())?;
1✔
129
        }
1✔
130
        Ok(())
21✔
131
    }
21✔
132
}
133

134
/// A builder to generate a knowledge base course and associated assets.
135
pub struct CourseBuilder {
136
    /// Base name of the directory on which to store this course.
137
    pub directory_name: String,
138

139
    /// The builders for the lessons in this course.
140
    pub lessons: Vec<LessonBuilder>,
141

142
    /// The assets associated with this course.
143
    pub assets: Vec<AssetBuilder>,
144

145
    /// The manifest for this course.
146
    pub manifest: CourseManifest,
147
}
148

149
impl CourseBuilder {
150
    /// Writes the files needed for this course to the given directory.
151
    pub fn build(&self, parent_directory: &Path) -> Result<()> {
3✔
152
        // Verify that the directory doesn't already exist and create it.
153
        let course_directory = parent_directory.join(&self.directory_name);
3✔
154
        ensure!(
3✔
155
            !course_directory.is_dir(),
3✔
156
            "course directory {} already exists",
157
            course_directory.display(), // grcov-excl-line
158
        );
159
        create_dir_all(&course_directory)?;
3✔
160

161
        // Write all the assets.
162
        for builder in &self.assets {
5✔
163
            builder.build(&course_directory)?;
2✔
164
        }
165

166
        // For each lesson in the course, create a directory with the name
167
        // `<LESSON_SHORT_ID>.lesson` and build the lesson in that directory.
168
        for builder in &self.lessons {
24✔
169
            let lesson_directory =
170
                course_directory.join(format!("{}{}", builder.lesson.short_id, LESSON_SUFFIX));
21✔
171
            fs::create_dir_all(&lesson_directory)?;
21✔
172
            builder.build(&lesson_directory)?;
21✔
173
        }
21✔
174

175
        // Write the manifest to disk.
176
        let manifest_json = serde_json::to_string_pretty(&self.manifest)? + "\n";
3✔
177
        let manifest_path = course_directory.join("course_manifest.json");
3✔
178
        let mut manifest_file = File::create(manifest_path)?;
3✔
179
        manifest_file.write_all(manifest_json.as_bytes())?;
3✔
180
        Ok(())
3✔
181
    }
3✔
182
}
183

184
#[cfg(test)]
185
mod test {
186
    use std::collections::BTreeMap;
187

188
    use anyhow::Result;
189

190
    use crate::{
191
        course_builder::knowledge_base_builder::*,
192
        data::{course_generator::knowledge_base::KnowledgeBaseFile, ExerciseType},
193
    };
194

195
    /// Creates a test lesson builder.
196
    fn test_lesson_builder() -> LessonBuilder {
1✔
197
        let exercise_builder = ExerciseBuilder {
1✔
198
            exercise: KnowledgeBaseExercise {
1✔
199
                short_id: "ex1".to_string(),
1✔
200
                short_lesson_id: "lesson1".into(),
1✔
201
                course_id: "course1".into(),
1✔
202
                front_file: "ex1.front.md".to_string(),
1✔
203
                back_file: Some("ex1.back.md".to_string()),
1✔
204
                name: Some("Exercise 1".to_string()),
1✔
205
                description: Some("Exercise 1 description".to_string()),
1✔
206
                exercise_type: Some(ExerciseType::Procedural),
207
            },
208
            asset_builders: vec![
2✔
209
                AssetBuilder {
1✔
210
                    file_name: "ex1.front.md".to_string(),
1✔
211
                    contents: "Exercise 1 front".to_string(),
1✔
212
                },
213
                AssetBuilder {
1✔
214
                    file_name: "ex1.back.md".to_string(),
1✔
215
                    contents: "Exercise 1 back".to_string(),
1✔
216
                },
217
            ],
218
        };
219
        LessonBuilder {
1✔
220
            lesson: KnowledgeBaseLesson {
1✔
221
                short_id: "lesson1".into(),
1✔
222
                course_id: "course1".into(),
1✔
223
                name: Some("Lesson 1".to_string()),
1✔
224
                description: Some("Lesson 1 description".to_string()),
1✔
225
                dependencies: Some(vec!["lesson2".into()]),
1✔
226
                metadata: Some(BTreeMap::from([(
1✔
227
                    "key".to_string(),
1✔
228
                    vec!["value".to_string()],
1✔
229
                )])),
230
                instructions: Some("instructions.md".to_string()),
1✔
231
                material: Some("material.md".to_string()),
1✔
232
            },
233
            exercises: vec![exercise_builder],
1✔
234
            assets: vec![
2✔
235
                AssetBuilder {
1✔
236
                    file_name: "instructions.md".to_string(),
1✔
237
                    contents: "Instructions".to_string(),
1✔
238
                },
239
                AssetBuilder {
1✔
240
                    file_name: "material.md".to_string(),
1✔
241
                    contents: "Material".to_string(),
1✔
242
                },
243
            ],
244
        }
245
    }
1✔
246

247
    /// Verifies that the course builder writes the correct files to disk.
248
    #[test]
249
    fn course_builder() -> Result<()> {
2✔
250
        let temp_dir = tempfile::tempdir()?;
1✔
251
        let course_builder = CourseBuilder {
1✔
252
            directory_name: "course1".into(),
1✔
253
            manifest: CourseManifest {
1✔
254
                id: "course1".into(),
1✔
255
                name: "Course 1".into(),
1✔
256
                dependencies: vec![],
1✔
257
                description: None,
1✔
258
                authors: None,
1✔
259
                metadata: None,
1✔
260
                course_material: None,
1✔
261
                course_instructions: None,
1✔
262
                generator_config: None,
1✔
263
            },
264
            lessons: vec![test_lesson_builder()],
1✔
265
            assets: vec![
2✔
266
                AssetBuilder {
1✔
267
                    file_name: "course_instructions.md".to_string(),
1✔
268
                    contents: "Course Instructions".to_string(),
1✔
269
                },
270
                AssetBuilder {
1✔
271
                    file_name: "course_material.md".to_string(),
1✔
272
                    contents: "Course Material".to_string(),
1✔
273
                },
274
            ],
275
        };
276

277
        course_builder.build(temp_dir.path())?;
1✔
278

279
        // Verify that the exercise was built correctly.
280
        let course_dir = temp_dir.path().join("course1");
1✔
281
        let lesson_dir = course_dir.join("lesson1.lesson");
1✔
282
        assert!(lesson_dir.exists());
1✔
283
        assert!(lesson_dir.join("ex1.front.md").exists());
1✔
284
        assert_eq!(
1✔
285
            fs::read_to_string(lesson_dir.join("ex1.front.md"))?,
1✔
286
            "Exercise 1 front"
287
        );
288
        assert!(lesson_dir.join("ex1.back.md").exists());
1✔
289
        assert_eq!(
1✔
290
            fs::read_to_string(lesson_dir.join("ex1.back.md"))?,
1✔
291
            "Exercise 1 back"
292
        );
293
        assert!(lesson_dir.join("ex1.name.json").exists());
1✔
294
        assert_eq!(
1✔
295
            KnowledgeBaseFile::open::<String>(&lesson_dir.join("ex1.name.json"))?,
1✔
296
            "Exercise 1",
297
        );
298
        assert!(lesson_dir.join("ex1.description.json").exists());
1✔
299
        assert_eq!(
1✔
300
            KnowledgeBaseFile::open::<String>(&lesson_dir.join("ex1.description.json"))?,
1✔
301
            "Exercise 1 description",
302
        );
303
        assert!(lesson_dir.join("ex1.type.json").exists());
1✔
304
        assert_eq!(
1✔
305
            KnowledgeBaseFile::open::<ExerciseType>(&lesson_dir.join("ex1.type.json"))?,
1✔
306
            ExerciseType::Procedural,
307
        );
308

309
        // Verify that the lesson was built correctly.
310
        assert!(lesson_dir.join(LESSON_NAME_FILE).exists());
1✔
311
        assert_eq!(
1✔
312
            KnowledgeBaseFile::open::<String>(&lesson_dir.join(LESSON_NAME_FILE))?,
1✔
313
            "Lesson 1",
314
        );
315
        assert!(lesson_dir.join(LESSON_DESCRIPTION_FILE).exists());
1✔
316
        assert_eq!(
1✔
317
            KnowledgeBaseFile::open::<String>(&lesson_dir.join(LESSON_DESCRIPTION_FILE))?,
1✔
318
            "Lesson 1 description",
319
        );
320
        assert!(lesson_dir.join(LESSON_DEPENDENCIES_FILE).exists());
1✔
321
        assert_eq!(
1✔
322
            KnowledgeBaseFile::open::<Vec<String>>(&lesson_dir.join(LESSON_DEPENDENCIES_FILE))?,
1✔
323
            vec!["lesson2".to_string()],
1✔
324
        );
325
        assert!(lesson_dir.join(LESSON_METADATA_FILE).exists());
1✔
326
        assert_eq!(
1✔
327
            KnowledgeBaseFile::open::<BTreeMap<String, Vec<String>>>(
1✔
328
                &lesson_dir.join(LESSON_METADATA_FILE)
1✔
329
            )
330
            .unwrap(),
331
            BTreeMap::from([("key".to_string(), vec!["value".to_string()])]),
1✔
332
        );
333
        assert!(lesson_dir.join(LESSON_INSTRUCTIONS_FILE).exists());
1✔
334
        assert_eq!(
1✔
335
            fs::read_to_string(lesson_dir.join("instructions.md"))?,
1✔
336
            "Instructions",
337
        );
338
        assert_eq!(
1✔
339
            KnowledgeBaseFile::open::<String>(&lesson_dir.join(LESSON_INSTRUCTIONS_FILE))?,
1✔
340
            "instructions.md",
341
        );
342
        assert!(lesson_dir.join(LESSON_MATERIAL_FILE).exists());
1✔
343
        assert_eq!(
1✔
344
            fs::read_to_string(lesson_dir.join("material.md"))?,
1✔
345
            "Material",
346
        );
347
        assert_eq!(
1✔
348
            KnowledgeBaseFile::open::<String>(&lesson_dir.join(LESSON_MATERIAL_FILE))?,
1✔
349
            "material.md",
350
        );
351

352
        // Verify that the course was built correctly.
353
        assert!(course_dir.join("course_manifest.json").exists());
1✔
354
        assert_eq!(
1✔
355
            KnowledgeBaseFile::open::<CourseManifest>(&course_dir.join("course_manifest.json"))
1✔
356
                .unwrap(),
357
            course_builder.manifest,
358
        );
359
        assert!(course_dir.join("course_instructions.md").exists());
1✔
360
        assert_eq!(
1✔
361
            fs::read_to_string(course_dir.join("course_instructions.md"))?,
1✔
362
            "Course Instructions",
363
        );
364
        assert!(course_dir.join("course_material.md").exists());
1✔
365
        assert_eq!(
1✔
366
            fs::read_to_string(course_dir.join("course_material.md"))?,
1✔
367
            "Course Material",
368
        );
369

370
        Ok(())
1✔
371
    }
2✔
372
}
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

© 2026 Coveralls, Inc