1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::Content;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "lowercase")]
14#[non_exhaustive]
15pub enum Question {
16 Noul(Noul),
18 Choice(Choice),
20 Score(Score),
22}
23
24impl From<Noul> for Question {
25 fn from(question: Noul) -> Self {
26 Question::Noul(question)
27 }
28}
29
30impl From<Choice> for Question {
31 fn from(question: Choice) -> Self {
32 Question::Choice(question)
33 }
34}
35
36impl From<Score> for Question {
37 fn from(question: Score) -> Self {
38 Question::Score(question)
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct Noul {
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub instructions: Option<Content>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub criteria: Option<NoulCriteria>,
69}
70
71impl Noul {
72 pub fn new(instructions: impl Into<Content>) -> Self {
74 Noul { instructions: Some(instructions.into()), criteria: None }
75 }
76
77 #[must_use]
79 pub fn yes(mut self, meaning: impl Into<Content>) -> Self {
80 self.criteria.get_or_insert_with(NoulCriteria::default).yes = Some(meaning.into());
81 self
82 }
83
84 #[must_use]
86 pub fn no(mut self, meaning: impl Into<Content>) -> Self {
87 self.criteria.get_or_insert_with(NoulCriteria::default).no = Some(meaning.into());
88 self
89 }
90}
91
92#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
94#[non_exhaustive]
95pub struct NoulCriteria {
96 #[serde(rename = "true", default, skip_serializing_if = "Option::is_none")]
98 pub yes: Option<Content>,
99 #[serde(rename = "false", default, skip_serializing_if = "Option::is_none")]
101 pub no: Option<Content>,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[non_exhaustive]
125pub struct Choice {
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub instructions: Option<Content>,
129 pub criteria: IndexMap<String, Option<Content>>,
132}
133
134impl Choice {
135 pub fn new<N, D>(instructions: impl Into<Content>, options: impl IntoIterator<Item = (N, D)>) -> Self
137 where
138 N: Into<String>,
139 D: Into<Content>,
140 {
141 let criteria = options.into_iter().map(|(name, description)| (name.into(), Some(description.into()))).collect();
142 Choice { instructions: Some(instructions.into()), criteria }
143 }
144
145 pub fn labels<N: Into<String>>(instructions: impl Into<Content>, names: impl IntoIterator<Item = N>) -> Self {
147 let criteria = names.into_iter().map(|name| (name.into(), None)).collect();
148 Choice { instructions: Some(instructions.into()), criteria }
149 }
150
151 #[must_use]
153 pub fn option(mut self, name: impl Into<String>, description: impl Into<Content>) -> Self {
154 self.criteria.insert(name.into(), Some(description.into()));
155 self
156 }
157
158 #[must_use]
160 pub fn label(mut self, name: impl Into<String>) -> Self {
161 self.criteria.insert(name.into(), None);
162 self
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[non_exhaustive]
180pub struct Score {
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub instructions: Option<Content>,
184 pub criteria: Vec<Content>,
186}
187
188impl Score {
189 pub fn new<L: Into<Content>>(instructions: impl Into<Content>, levels: impl IntoIterator<Item = L>) -> Self {
191 Score { instructions: Some(instructions.into()), criteria: levels.into_iter().map(Into::into).collect() }
192 }
193}
194
195#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
212#[serde(transparent)]
213pub struct Questions(IndexMap<String, Question>);
214
215impl Questions {
216 pub fn new() -> Self {
218 Questions::default()
219 }
220
221 #[must_use]
223 pub fn with(mut self, id: impl Into<String>, question: impl Into<Question>) -> Self {
224 self.insert(id, question);
225 self
226 }
227
228 pub fn insert(&mut self, id: impl Into<String>, question: impl Into<Question>) -> Option<Question> {
230 self.0.insert(id.into(), question.into())
231 }
232
233 pub fn get(&self, id: &str) -> Option<&Question> {
235 self.0.get(id)
236 }
237
238 pub fn len(&self) -> usize {
240 self.0.len()
241 }
242
243 pub fn is_empty(&self) -> bool {
245 self.0.is_empty()
246 }
247
248 pub fn ids(&self) -> impl Iterator<Item = &str> {
250 self.0.keys().map(String::as_str)
251 }
252
253 pub fn iter(&self) -> indexmap::map::Iter<'_, String, Question> {
255 self.0.iter()
256 }
257
258 pub fn as_map(&self) -> &IndexMap<String, Question> {
260 &self.0
261 }
262}
263
264impl From<IndexMap<String, Question>> for Questions {
265 fn from(questions: IndexMap<String, Question>) -> Self {
266 Questions(questions)
267 }
268}
269
270impl From<Questions> for IndexMap<String, Question> {
271 fn from(questions: Questions) -> Self {
272 questions.0
273 }
274}
275
276impl<I: Into<String>, Q: Into<Question>> FromIterator<(I, Q)> for Questions {
277 fn from_iter<T: IntoIterator<Item = (I, Q)>>(questions: T) -> Self {
278 Questions(questions.into_iter().map(|(id, question)| (id.into(), question.into())).collect())
279 }
280}
281
282impl<I: Into<String>, Q: Into<Question>> Extend<(I, Q)> for Questions {
283 fn extend<T: IntoIterator<Item = (I, Q)>>(&mut self, questions: T) {
284 self.0.extend(questions.into_iter().map(|(id, question)| (id.into(), question.into())));
285 }
286}
287
288impl IntoIterator for Questions {
289 type Item = (String, Question);
290 type IntoIter = indexmap::map::IntoIter<String, Question>;
291
292 fn into_iter(self) -> Self::IntoIter {
293 self.0.into_iter()
294 }
295}
296
297impl<'a> IntoIterator for &'a Questions {
298 type Item = (&'a String, &'a Question);
299 type IntoIter = indexmap::map::Iter<'a, String, Question>;
300
301 fn into_iter(self) -> Self::IntoIter {
302 self.0.iter()
303 }
304}