moodle_xml/
quiz.rs

1use crate::question::QuestionType;
2use std::fs::File;
3use std::{fmt, ops::Deref};
4use xml::writer::{EmitterConfig, XmlEvent};
5
6/// Error type for Quiz, Question and Answer struct
7///
8/// ### Errors
9///
10/// XMLWriterError ```xml::writer::Error``` - xml-rs writer error
11///
12/// ```EmptyError``` - Error when generating empty quiz or question
13///
14/// ```ValueError``` - Error when generating answer with too much points
15/// AnswerFractionError - Error when answer fraction is larger than 100
16/// AnswerCountError - Error when answer count is different than required
17#[derive(Debug)]
18pub enum QuizError {
19    XMLWriterError(xml::writer::Error),
20    EmptyError(String),
21    ValueError(String),
22    AnswerFractionError(String),
23    AnswerCountError(String),
24}
25impl From<xml::writer::Error> for QuizError {
26    fn from(e: xml::writer::Error) -> Self {
27        QuizError::XMLWriterError(e)
28    }
29}
30impl From<EmptyError> for QuizError {
31    fn from(e: EmptyError) -> Self {
32        QuizError::EmptyError(e.to_string())
33    }
34}
35impl From<ValueError> for QuizError {
36    fn from(e: ValueError) -> Self {
37        QuizError::ValueError(e.to_string())
38    }
39}
40
41#[derive(Debug)]
42pub struct EmptyError;
43
44impl fmt::Display for EmptyError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str("Quiz questions or answer is empty")
47    }
48}
49
50#[derive(Debug)]
51pub struct ValueError;
52
53impl fmt::Display for ValueError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str("Answer type has value outside of limits")
56    }
57}
58
59/// A category for the quiz, can be used to categorize questions.
60/// A string type is used internally to represent the category.
61#[derive(Debug, Clone)]
62pub struct Category(String);
63
64impl Deref for Category {
65    type Target = String;
66
67    fn deref(&self) -> &Self::Target {
68        &self.0
69    }
70}
71impl From<String> for Category {
72    fn from(s: String) -> Self {
73        Category(s)
74    }
75}
76impl From<&str> for Category {
77    fn from(s: &str) -> Self {
78        Category(s.to_string())
79    }
80}
81impl From<Category> for Vec<Category> {
82    fn from(category: Category) -> Self {
83        vec![category]
84    }
85}
86
87/// A quiz struct that contains a vector of questions and optional categories.
88pub struct Quiz {
89    /// A vector of questions, can be any type of a question
90    questions: Vec<QuestionType>,
91    categories: Option<Vec<Category>>,
92}
93impl Quiz {
94    /// Creates a new quiz instance with the specified moodle categories and questions.
95    /// Categories are not mandatory.
96    /// See [Moodle XML format](https://docs.moodle.org/404/en/Moodle_XML_format) for more information.
97    /// Category entry is appended after `$course$/` mark.
98    pub fn new(questions: Vec<QuestionType>) -> Self {
99        Self {
100            questions,
101            categories: None,
102        }
103    }
104    /// Adds categories to the quiz.
105    pub fn set_categories(&mut self, categories: Vec<Category>) {
106        self.categories = Some(categories);
107    }
108    /// Creates an XML file from quiz object, containing question and answer objects.
109    ///
110    /// # Arguments
111    ///
112    /// - `filename`: The name of the XML file, including whole filepath.
113    ///
114    /// # Errors
115    ///
116    /// Returns an QuizError if the problem occurs during writing the XML file or requirements are not met.
117    pub fn to_xml(&mut self, filename: &str) -> Result<(), QuizError> {
118        if self.questions.is_empty() {
119            return Err(EmptyError.into());
120        }
121        let output: File = File::create(filename)
122            .unwrap_or_else(|e| panic!("Bad file path: {} More: {}", filename, e));
123        let mut writer = EmitterConfig::new()
124            .perform_indent(true)
125            .create_writer(&output);
126
127        writer.write(XmlEvent::start_element("quiz"))?;
128        if let Some(categories) = self.categories.as_ref() {
129            for category in categories {
130                writer.write(XmlEvent::start_element("question").attr("type", "category"))?;
131                writer.write(XmlEvent::start_element("category"))?;
132                writer.write(XmlEvent::start_element("text"))?;
133                let string = ["$course$/", category.as_str(), "/"].concat();
134                writer.write(XmlEvent::characters(string.as_str()))?;
135                writer.write(XmlEvent::end_element())?;
136                writer.write(XmlEvent::end_element())?;
137                writer.write(XmlEvent::end_element())?;
138            }
139        }
140
141        if self.questions.is_empty() {
142            return Err(EmptyError.into());
143        }
144        for question in &self.questions {
145            question.to_xml(&mut writer)?;
146        }
147        writer.write(XmlEvent::end_element())?;
148        Ok(())
149    }
150}