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

trane-project / trane / 3774657368

pending completion
3774657368

Pull #162

github

GitHub
Merge f684ca31b into a872cb5b2
Pull Request #162: Make MusicPassage a struct and not an enum

14 of 14 new or added lines in 1 file covered. (100.0%)

4215 of 4216 relevant lines covered (99.98%)

102096.02 hits per line

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

98.59
/src/data/course_generator/music_piece.rs
1
//! Contains the logic for generating a course to learn a piece of music.
2
//!
3
//! Given a piece of music and the passages and sub-passages in which it is divided, this module
4
//! generates a course that allows the user to learn the piece of music by first practicing the
5
//! smallest passages and then working up until the full piece is mastered.
6

7
use anyhow::Result;
8
use indoc::{formatdoc, indoc};
9
use serde::{Deserialize, Serialize};
10
use std::{collections::HashMap, path::Path};
11
use ustr::Ustr;
12

13
use crate::data::{
14
    BasicAsset, CourseManifest, ExerciseAsset, ExerciseManifest, ExerciseType, GenerateManifests,
15
    GeneratedCourse, LessonManifest, UserPreferences,
16
};
17

18
/// The common instructions for all lessons in the course.
19
const INSTRUCTIONS: &str = indoc! {"
20
    Given the following passage from the piece, start by listening to it repeatedly
21
    until you can audiate it clearly in your head. You can also attempt to hum or
22
    sing it if possible. Then, play the passage on your instrument.
23
"};
24

25
//@<music-asset
26
/// Represents a music asset to be practiced.
27
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28
pub enum MusicAsset {
29
    /// A link to a SoundSlice.
30
    SoundSlice(String),
37✔
31

32
    /// The path to a local file. For example, the path to a PDF of the sheet music.
33
    LocalFile(String),
8✔
34
}
35
//>@music-asset
36

37
impl MusicAsset {
38
    /// Generates an exercise asset from this music asset.
39
    pub fn generate_exercise_asset(&self, start: &str, end: &str) -> ExerciseAsset {
45✔
40
        match self {
45✔
41
            MusicAsset::SoundSlice(url) => {
44✔
42
                let description = formatdoc! {"
44✔
43
                    {}
44

45
                    - Passage start: {}
46
                    - Passage end: {}
47
                ", INSTRUCTIONS, start, end};
48
                ExerciseAsset::SoundSliceAsset {
44✔
49
                    link: url.clone(),
44✔
50
                    description: Some(description),
44✔
51
                    backup: None,
44✔
52
                }
53
            }
44✔
54
            MusicAsset::LocalFile(path) => {
1✔
55
                let description = formatdoc! {"
1✔
56
                    {}
57

58
                    - Passage start: {}
59
                    - Passage end: {}
60
                    
61
                    The file containing the music sheet is located at {}. Relative paths are
62
                    relative to the root of the course.
63
                ", INSTRUCTIONS, start, end, path};
64
                ExerciseAsset::BasicAsset(BasicAsset::InlinedAsset {
1✔
65
                    content: description,
1✔
66
                })
67
            }
1✔
68
        }
69
    }
45✔
70
}
71

72
//@<music-passage
73
/// Represents a music passage to be practiced.
74
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
75
pub struct MusicPassage {
76
    /// The start of the passage.
77
    pub start: String,
420✔
78

79
    /// The end of the passage.
80
    pub end: String,
420✔
81

82
    /// The sub-passages that must be mastered before this passage can be mastered. Each
83
    /// sub-passage should be given a unique index which will be used to generate the lesson ID.
84
    /// Those values should not change once they are defined or progress for this lesson will be
85
    /// lost. This value is a map instead of a list because rearranging the order of the
86
    /// passages in a list would also change the IDs of the generated lessons.
87
    pub sub_passages: HashMap<usize, MusicPassage>,
420✔
88
}
89
//>@music-passage
90

91
impl MusicPassage {
92
    /// Generates the lesson ID for this course and passage, identified by the given path.
93
    pub fn generate_lesson_id(
85✔
94
        &self,
95
        course_manifest: &CourseManifest,
96
        passage_path: Vec<usize>,
97
    ) -> Ustr {
98
        // An empty passage path means the course consists of only a lesson. Give this lesson a
99
        // hard-coded ID.
100
        if passage_path.is_empty() {
85✔
101
            return Ustr::from(&format!("{}::lesson", course_manifest.id));
×
102
        }
103

104
        // Otherwise, generate the lesson ID from the passage path.
105
        let mut lesson_id = "".to_string();
85✔
106
        for index in passage_path {
374✔
107
            lesson_id.push_str(&format!("::{}", index));
289✔
108
        }
109
        Ustr::from(&format!("{}::{}", course_manifest.id, lesson_id))
85✔
110
    }
85✔
111

112
    /// Generates the lesson and exercise manifests for this passage, recursively doing so if the
113
    /// dependencies are not empty.
114
    pub fn generate_lesson_helper(
45✔
115
        &self,
116
        course_manifest: &CourseManifest,
117
        passage_path: Vec<usize>,
118
        sub_passages: &HashMap<usize, MusicPassage>,
119
        music_asset: &MusicAsset,
120
    ) -> Vec<(LessonManifest, Vec<ExerciseManifest>)> {
121
        // Recursively generate the dependency lessons and IDs.
122
        let mut lessons = vec![];
45✔
123
        let mut dependency_ids = vec![];
45✔
124
        for (index, sub_passage) in sub_passages {
85✔
125
            // Create the dependency path.
126
            let mut dependency_path = passage_path.clone();
40✔
127
            dependency_path.push(*index);
40✔
128

129
            // Generate the dependency ID and lessons.
130
            dependency_ids
40✔
131
                .push(sub_passage.generate_lesson_id(course_manifest, dependency_path.clone()));
40✔
132
            lessons.append(&mut sub_passage.generate_lesson_helper(
40✔
133
                course_manifest,
134
                dependency_path,
40✔
135
                &sub_passage.sub_passages,
40✔
136
                music_asset,
137
            ));
40✔
138
        }
139

140
        // Create the lesson and exercise manifests for this passage and add them to the list.
141
        let lesson_manifest = LessonManifest {
45✔
142
            id: self.generate_lesson_id(course_manifest, passage_path),
45✔
143
            course_id: course_manifest.id,
45✔
144
            name: course_manifest.name.clone(),
45✔
145
            description: None,
45✔
146
            dependencies: dependency_ids,
45✔
147
            metadata: None,
45✔
148
            lesson_instructions: None,
45✔
149
            lesson_material: None,
45✔
150
        };
151
        let exercise_manifest = ExerciseManifest {
45✔
152
            id: Ustr::from(&format!("{}::exercise", lesson_manifest.id)),
45✔
153
            lesson_id: lesson_manifest.id,
45✔
154
            course_id: course_manifest.id,
45✔
155
            name: course_manifest.name.clone(),
45✔
156
            description: None,
45✔
157
            exercise_type: ExerciseType::Procedural,
45✔
158
            exercise_asset: music_asset.generate_exercise_asset(&self.start, &self.end),
45✔
159
        };
45✔
160
        lessons.push((lesson_manifest, vec![exercise_manifest]));
45✔
161

162
        lessons
163
    }
45✔
164

165
    /// Generates the lesson and exercise manifests for this passage.
166
    pub fn generate_lessons(
5✔
167
        &self,
168
        course_manifest: &CourseManifest,
169
        music_asset: &MusicAsset,
170
    ) -> Vec<(LessonManifest, Vec<ExerciseManifest>)> {
171
        // Use a starting path of [0].
172
        self.generate_lesson_helper(course_manifest, vec![0], &self.sub_passages, music_asset)
5✔
173
    }
5✔
174
}
175

176
//@<music-piece-config
177
/// The config to create a course that teaches a piece of music.
178
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
179
pub struct MusicPieceConfig {
180
    /// The asset containing the music to be practiced.
181
    pub music_asset: MusicAsset,
40✔
182

183
    /// The passages in which the music is divided for practice.
184
    pub passages: MusicPassage,
40✔
185
}
186
//>@music-piece-config
187

188
impl GenerateManifests for MusicPieceConfig {
189
    fn generate_manifests(
5✔
190
        &self,
191
        _course_root: &Path,
192
        course_manifest: &CourseManifest,
193
        _preferences: &UserPreferences,
194
    ) -> Result<GeneratedCourse> {
195
        Ok(GeneratedCourse {
5✔
196
            lessons: self
5✔
197
                .passages
198
                .generate_lessons(course_manifest, &self.music_asset),
199
            updated_instructions: None,
5✔
200
            updated_metadata: None,
5✔
201
        })
202
    }
5✔
203
}
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