moodle_xml/
quiz.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use crate::question::QuestionType;
use std::fs::File;
use std::{fmt, ops::Deref};
use xml::writer::{EmitterConfig, XmlEvent};

/// Error type for Quiz, Question and Answer struct
///
/// ### Errors
///
/// XMLWriterError ```xml::writer::Error``` - xml-rs writer error
///
/// ```EmptyError``` - Error when generating empty quiz or question
///
/// ```ValueError``` - Error when generating answer with too much points
/// AnswerFractionError - Error when answer fraction is larger than 100
/// AnswerCountError - Error when answer count is different than required
#[derive(Debug)]
pub enum QuizError {
    XMLWriterError(xml::writer::Error),
    EmptyError(String),
    ValueError(String),
    AnswerFractionError(String),
    AnswerCountError(String),
}
impl From<xml::writer::Error> for QuizError {
    fn from(e: xml::writer::Error) -> Self {
        QuizError::XMLWriterError(e)
    }
}
impl From<EmptyError> for QuizError {
    fn from(e: EmptyError) -> Self {
        QuizError::EmptyError(e.to_string())
    }
}
impl From<ValueError> for QuizError {
    fn from(e: ValueError) -> Self {
        QuizError::ValueError(e.to_string())
    }
}

#[derive(Debug)]
pub struct EmptyError;

impl fmt::Display for EmptyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Quiz questions or answer is empty")
    }
}

#[derive(Debug)]
pub struct ValueError;

impl fmt::Display for ValueError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Answer type has value outside of limits")
    }
}

/// A category for the quiz, can be used to categorize questions.
/// A string type is used internally to represent the category.
#[derive(Debug, Clone)]
pub struct Category(String);

impl Deref for Category {
    type Target = String;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl From<String> for Category {
    fn from(s: String) -> Self {
        Category(s)
    }
}
impl From<&str> for Category {
    fn from(s: &str) -> Self {
        Category(s.to_string())
    }
}
impl From<Category> for Vec<Category> {
    fn from(category: Category) -> Self {
        vec![category]
    }
}

/// A quiz struct that contains a vector of questions and optional categories.
pub struct Quiz {
    /// A vector of questions, can be any type of a question
    questions: Vec<QuestionType>,
    categories: Option<Vec<Category>>,
}
impl Quiz {
    /// Creates a new quiz instance with the specified moodle categories and questions.
    /// Categories are not mandatory.
    /// See [Moodle XML format](https://docs.moodle.org/404/en/Moodle_XML_format) for more information.
    /// Category entry is appended after `$course$/` mark.
    pub fn new(questions: Vec<QuestionType>) -> Self {
        Self {
            questions,
            categories: None,
        }
    }
    /// Adds categories to the quiz.
    pub fn set_categories(&mut self, categories: Vec<Category>) {
        self.categories = Some(categories);
    }
    /// Creates an XML file from quiz object, containing question and answer objects.
    ///
    /// # Arguments
    ///
    /// - `filename`: The name of the XML file, including whole filepath.
    ///
    /// # Errors
    ///
    /// Returns an QuizError if the problem occurs during writing the XML file or requirements are not met.

    pub fn to_xml(&mut self, filename: &str) -> Result<(), QuizError> {
        if self.questions.is_empty() {
            return Err(EmptyError.into());
        }
        let output: File = File::create(filename)
            .unwrap_or_else(|e| panic!("Bad file path: {} More: {}", filename, e));
        let mut writer = EmitterConfig::new()
            .perform_indent(true)
            .create_writer(&output);

        writer.write(XmlEvent::start_element("quiz"))?;
        if let Some(categories) = self.categories.as_ref() {
            for category in categories {
                writer.write(XmlEvent::start_element("question").attr("type", "category"))?;
                writer.write(XmlEvent::start_element("category"))?;
                writer.write(XmlEvent::start_element("text"))?;
                let string = ["$course$/", category.as_str(), "/"].concat();
                writer.write(XmlEvent::characters(string.as_str()))?;
                writer.write(XmlEvent::end_element())?;
                writer.write(XmlEvent::end_element())?;
                writer.write(XmlEvent::end_element())?;
            }
        }

        if self.questions.is_empty() {
            return Err(EmptyError.into());
        }
        for question in &self.questions {
            question.to_xml(&mut writer)?;
        }
        writer.write(XmlEvent::end_element())?;
        Ok(())
    }
}