konnektoren_core/challenges/gap_fill/
mod.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)]
5pub struct GapFill {
6 pub id: String,
8 pub name: String,
10 pub description: String,
12 pub lang: String,
14 pub questions: Vec<GapFillQuestion>,
16}
17
18impl Default for GapFill {
19 fn default() -> Self {
20 let data = include_str!("../../../assets/gap_fill_default.yml");
21 serde_yaml::from_str(data).unwrap()
22 }
23}
24
25#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, JsonSchema)]
26pub struct GapFillQuestion {
27 pub sentence: String,
29 pub gaps: Vec<Gap>,
31 pub hints: Vec<String>,
33 pub translation: String,
35 pub explanation: String,
37}
38
39#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, JsonSchema)]
40pub struct Gap {
41 pub position: usize,
43 pub options: Vec<String>,
45 pub correct: String,
47}
48
49#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, JsonSchema)]
50pub struct GapFillAnswer {
51 pub question_index: usize,
53 pub answers: Vec<String>,
55}
56
57impl GapFill {
58 pub fn check_answer(&self, answer: &GapFillAnswer) -> bool {
59 if let Some(question) = self.questions.get(answer.question_index) {
60 if question.gaps.len() != answer.answers.len() {
61 return false;
62 }
63
64 question
65 .gaps
66 .iter()
67 .zip(answer.answers.iter())
68 .all(|(gap, ans)| gap.correct == *ans)
69 } else {
70 false
71 }
72 }
73
74 pub fn get_feedback(&self, answer: &GapFillAnswer) -> String {
75 if let Some(question) = self.questions.get(answer.question_index) {
76 if self.check_answer(answer) {
77 format!("Correct! {}", question.explanation)
78 } else {
79 let mut feedback = String::from("Incorrect. Hints:\n");
80 for hint in &question.hints {
81 feedback.push_str(&format!("- {}\n", hint));
82 }
83 feedback
84 }
85 } else {
86 "Invalid question index".to_string()
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_gap_fill_deserialization() {
97 let yaml = r#"
98 id: "past-tense"
99 name: "Past Tense Exercise"
100 description: "Fill in the correct past tense forms"
101 lang: "de"
102 questions:
103 - sentence: "Ich __ nach Berlin __ (fahren)."
104 gaps:
105 - position: 0
106 options: ["bin", "habe", "war"]
107 correct: "bin"
108 - position: 1
109 options: ["gefahren", "gefahrt", "fuhr"]
110 correct: "gefahren"
111 hints:
112 - "Movement verbs use 'sein' as auxiliary"
113 - "The past participle of 'fahren' is 'gefahren'"
114 translation: "I went to Berlin"
115 explanation: "We use 'sein' with verbs of movement and the past participle form"
116 "#;
117
118 let gap_fill: GapFill = serde_yaml::from_str(yaml).unwrap();
119 assert_eq!(gap_fill.id, "past-tense");
120 assert_eq!(gap_fill.questions.len(), 1);
121
122 let question = &gap_fill.questions[0];
123 assert_eq!(question.gaps.len(), 2);
124 assert_eq!(question.gaps[0].correct, "bin");
125 }
126
127 #[test]
128 fn test_check_answer() {
129 let gap_fill = GapFill {
130 id: "test".to_string(),
131 name: "Test".to_string(),
132 description: "Test".to_string(),
133 lang: "de".to_string(),
134 questions: vec![GapFillQuestion {
135 sentence: "Ich __ nach Berlin __ (fahren).".to_string(),
136 gaps: vec![
137 Gap {
138 position: 0,
139 options: vec!["bin".to_string(), "habe".to_string()],
140 correct: "bin".to_string(),
141 },
142 Gap {
143 position: 1,
144 options: vec!["gefahren".to_string(), "gefahrt".to_string()],
145 correct: "gefahren".to_string(),
146 },
147 ],
148 hints: vec!["Test hint".to_string()],
149 translation: "Test translation".to_string(),
150 explanation: "Test explanation".to_string(),
151 }],
152 };
153
154 let correct_answer = GapFillAnswer {
155 question_index: 0,
156 answers: vec!["bin".to_string(), "gefahren".to_string()],
157 };
158
159 let wrong_answer = GapFillAnswer {
160 question_index: 0,
161 answers: vec!["habe".to_string(), "gefahren".to_string()],
162 };
163
164 assert!(gap_fill.check_answer(&correct_answer));
165 assert!(!gap_fill.check_answer(&wrong_answer));
166 }
167
168 #[test]
169 fn test_get_feedback_correct_and_incorrect() {
170 let gap_fill = GapFill {
171 id: "test".to_string(),
172 name: "Test".to_string(),
173 description: "Test".to_string(),
174 lang: "de".to_string(),
175 questions: vec![GapFillQuestion {
176 sentence: "Ich __ nach Berlin __ (fahren).".to_string(),
177 gaps: vec![
178 Gap {
179 position: 0,
180 options: vec!["bin".to_string(), "habe".to_string()],
181 correct: "bin".to_string(),
182 },
183 Gap {
184 position: 1,
185 options: vec!["gefahren".to_string(), "gefahrt".to_string()],
186 correct: "gefahren".to_string(),
187 },
188 ],
189 hints: vec!["Test hint".to_string()],
190 translation: "Test translation".to_string(),
191 explanation: "Test explanation".to_string(),
192 }],
193 };
194
195 let correct_answer = GapFillAnswer {
196 question_index: 0,
197 answers: vec!["bin".to_string(), "gefahren".to_string()],
198 };
199 let wrong_answer = GapFillAnswer {
200 question_index: 0,
201 answers: vec!["habe".to_string(), "gefahren".to_string()],
202 };
203 let invalid_answer = GapFillAnswer {
204 question_index: 99,
205 answers: vec![],
206 };
207
208 assert!(
209 gap_fill
210 .get_feedback(&correct_answer)
211 .starts_with("Correct!")
212 );
213 assert!(
214 gap_fill
215 .get_feedback(&wrong_answer)
216 .starts_with("Incorrect.")
217 );
218 assert_eq!(
219 gap_fill.get_feedback(&invalid_answer),
220 "Invalid question index"
221 );
222 }
223}