mod question_bank;
mod helpers;
use std::error::Error;
use std::time::SystemTime;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::collections::hash_map::DefaultHasher;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ffi::OsString;
use std::process::Command;
use clap::ArgMatches;
use rand::prelude::*;
use rand_pcg::Lcg128Xsl64;
use question_bank::QuestionBank;
mod constants
{
pub const QUESTION_BANK_TAG_NAME_LONG: &str = "question_bank";
pub const QUESTION_BANK_TAG_NAME: &str = "qb";
pub const TITLE_ATTRIBUTE_NAME: &str = "title";
pub const DESCRIPTION_TAG_NAME_LONG: &str = "description";
pub const DESCRIPTION_TAG_NAME: &str = "d";
pub const QUESTION_GROUP_TAG_NAME_LONG: &str = "question_group";
pub const QUESTION_GROUP_TAG_NAME: &str = "qg";
pub const PICK_ATTRIBUTE_NAME: &str = "pick";
pub const SHUFFLE_ATTRIBUTE_NAME: &str = "shuffle";
pub const QUESTION_MC_TAG_NAME_LONG: &str = "question_mc";
pub const QUESTION_MC_TAG_NAME: &str = "qmc";
pub const QUESTION_VAR_TAG_NAME_LONG: &str = "question_var";
pub const QUESTION_VAR_TAG_NAME: &str = "qv";
pub const TEXT_FIELD_ATTRIBUTE_NAME_LONG: &str = "text_field_height";
pub const TEXT_FIELD_ATTRIBUTE_NAME: &str = "tfh";
pub const QUESTION_TEXT_TAG_NAME_LONG: &str = "question_text";
pub const QUESTION_TEXT_TAG_NAME: &str = "qt";
pub const ANSWER_MC_TAG_NAME_LONG: &str = "answer_mc";
pub const ANSWER_MC_TAG_NAME: &str = "amc";
pub const VAR_TEXT_TAG_NAME_LONG: &str = "var_text";
pub const VAR_TEXT_TAG_NAME: &str = "vt";
pub const TEXT_OPTION_TAG_NAME_LONG: &str = "option";
pub const TEXT_OPTION_TAG_NAME: &str = "o";
pub const FILE_AUTHOR: &str = "ТУЕС към ТУ-София";
pub const DEFAULT_TEXT_FIELD_HEIGHT: u16 = 5;
}
#[derive(Debug)]
pub enum VMKSError
{
InvalidVariants,
InvalidSeed,
LaTeXBuildError,
LaTeXCleanError,
UnexpectedTag
{
expected: String,
received: String
},
UnexpectedAttribute
{
expected: String,
received: String
},
UnexpectedText
{
expected: String,
received: String
},
PickTooMany,
InvalidPickValue
{
received: String
},
InvalidShuffleValue
{
received: String
},
GenericParseError
}
impl Display for VMKSError
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result
{
match self
{
VMKSError::InvalidVariants => write!(f, "invalid number of variants. Please provide a number greater than 0."),
VMKSError::InvalidSeed => write!(f, "invalid numeric seed. Use -S for non-numeric seeds."),
VMKSError::LaTeXBuildError => write!(f, "LaTeX was unable to generate a pdf file for one or more of the exam variants. Check its logs."),
VMKSError::LaTeXCleanError => write!(f, "Cleaning up LaTeX intermediate files failed."),
VMKSError::UnexpectedTag{expected, received} =>
if expected.is_empty()
{
write!(f, "unexpected tag. Expected no more tags, got \"{}\".", received)
}
else
{
write!(f, "unexpected tag. Expected {}, got \"{}\".", expected, received)
},
VMKSError::UnexpectedAttribute{expected, received} =>
if expected.is_empty()
{
write!(f, "unexpected attribute. Expected no attributes, got \"{}\".", received)
}
else
{
write!(f, "unexpected attribute. Expected {}, got \"{}\".", expected, received)
},
VMKSError::UnexpectedText{expected, received} =>
write!(f, "unexpected text. Expected {}, got \"{}\".", expected, received),
VMKSError::PickTooMany =>
write!(f, "specified number of questions to pick from a question group is larger than the number of questions in that group."),
VMKSError::InvalidPickValue{received} =>
write!(f, "invalid value provided for \"pick\" argument. Expected a non-negative integer, got \"{}\"", received),
VMKSError::InvalidShuffleValue {received} =>
write!(f, "invalid value provided for \"randomise\" argument. Expected a \"true\" or \"false\", got \"{}\"", received),
VMKSError::GenericParseError =>
write!(f, "unable to parse question bank file.")
}
}
}
impl Error for VMKSError {}
pub struct Config
{
pub seed: u64,
pub num_variants: u32,
pub plaintext: bool,
pub escape: bool,
pub question_bank_filename: OsString
}
impl Config
{
pub fn new(arguments: ArgMatches) -> Result<Config, Box<dyn Error>>
{
let num_variants = *arguments.try_get_one::<u32>("variants")?.unwrap();
let plaintext = *arguments.try_get_one::<bool>("plaintext")?.unwrap();
let escape = !*arguments.try_get_one::<bool>("no-escape")?.unwrap();
let question_bank_filename: OsString = arguments.try_get_one::<OsString>("question-bank")?.unwrap().clone();
if num_variants == 0
{
return Err(Box::new(VMKSError::InvalidVariants));
}
let mut seed: u64 = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
{
Ok(current_unix_time) => current_unix_time.as_secs(),
Err(system_time_error) => system_time_error.duration().as_secs()
};
if let Some(text_seed) = arguments.try_get_one::<String>("text-seed")?
{
let mut hasher = DefaultHasher::new();
text_seed.hash(&mut hasher);
seed = hasher.finish();
}
else if let Some(num_seed) = arguments.try_get_one::<u64>("num-seed")?
{
seed = *num_seed;
}
Ok(Config {
seed,
num_variants,
plaintext,
escape,
question_bank_filename
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>>
{
let mut rng = Lcg128Xsl64::seed_from_u64(config.seed);
let question_bank = QuestionBank::from_file(&config.question_bank_filename)?;
if config.plaintext
{
for i in 1..(config.num_variants + 1)
{
let mut file = File::create(format!("variant_{}.txt", i))?;
file.write_all(question_bank.random_variant_txt(&mut rng, config.escape).as_bytes())?;
}
}
else
{
let mut files: Vec<String> = Vec::new();
for i in 1..(config.num_variants + 1)
{
let filename = format!("variant_{}.tex", i);
files.push(filename.clone());
let mut file = File::create(&filename)?;
file.write_all(question_bank.random_variant_pdf(&mut rng, config.escape)?.as_bytes())?;
}
let mut latex_args: Vec<String> = vec![
"-silent".to_string(),
"-interaction=nonstopmode".to_string(),
"-pdflatex".to_string(),
];
latex_args.extend_from_slice(&files);
let latex_build_ret = Command::new("latexmk").args(latex_args).status()?;
if latex_build_ret.success()
{
let mut latex_args: Vec<String> = vec!["-c".to_string()];
latex_args.extend_from_slice(&files);
let latex_clean_ret = Command::new("latexmk").args(latex_args).status()?;
if !latex_clean_ret.success()
{
return Err(Box::new(VMKSError::LaTeXCleanError));
}
}
else
{
return Err(Box::new(VMKSError::LaTeXBuildError));
}
for file in files.iter()
{
fs::remove_file(file)?;
}
}
Ok(())
}