use db_objects::{Contest, Task, Taskgroup};
use serde_yaml;
use std::path::Path;
extern crate time;
#[derive(Debug, Deserialize)]
struct ContestYaml {
name: Option<String>,
participation_start: Option<String>,
participation_end: Option<String>,
review_start: Option<String>,
review_end: Option<String>,
duration_minutes: Option<i32>,
public_listing: Option<bool>,
protected: Option<bool>,
requires_login: Option<bool>,
requires_contest: Option<Vec<String>>,
secret: Option<String>,
message: Option<String>,
image: Option<String>,
language: Option<String>,
category: Option<String>,
min_grade: Option<i32>,
max_grade: Option<i32>,
position: Option<i32>,
team_participation: Option<bool>,
tags: Option<Vec<String>>,
tasks: Option<serde_yaml::Mapping>,
}
#[derive(Debug, Deserialize)]
struct TaskYaml {
name: Option<String>,
standalone: Option<bool>,
public_listing: Option<bool>,
position: Option<i32>,
image: Option<String>,
tags: Option<Vec<String>>,
languages: Option<Vec<String>>,
}
use self::time::{strptime, Timespec};
fn parse_timespec(time: String, key: &str, directory: &str, filename: &str) -> Timespec {
strptime(&time, &"%FT%T%z").map(|t| t.to_timespec())
.unwrap_or_else(|_| {
panic!("Time value '{}' could not be parsed in {}{}", key, directory, filename)
})
}
fn parse_contest_yaml(content: &str, filename: &str, directory: &str) -> Option<Vec<Contest>> {
let config: ContestYaml = match serde_yaml::from_str(&content) {
Ok(contest) => contest,
Err(e) => {
eprintln!();
eprintln!("{}", e);
eprintln!("Error loading contest YAML: {}{}", directory, filename);
panic!("Loading contest file")
}
};
let start: Option<Timespec> =
config.participation_start.map(|x| parse_timespec(x, "participation_start", directory, filename));
let end: Option<Timespec> =
config.participation_end.map(|x| parse_timespec(x, "participation_end", directory, filename));
let review_start: Option<Timespec> =
config.review_start.map(|x| parse_timespec(x, "review_start", directory, filename));
let review_end: Option<Timespec> = config.review_end.map(|x| parse_timespec(x, "review_end", directory, filename));
let review_start = if review_end.is_none() {
review_start
} else if let Some(end) = end {
Some(review_start.unwrap_or(end))
} else {
review_start
};
let mut contest =
Contest { id: None,
location: directory.to_string(),
filename: filename.to_string(),
name: config.name.unwrap_or_else(|| panic!("'name' missing in {}{}", directory, filename)),
duration:
config.duration_minutes
.unwrap_or_else(|| panic!("'duration_minutes' missing in {}{}", directory, filename)),
public: config.public_listing.unwrap_or(false),
start,
end,
review_start,
review_end,
min_grade: config.min_grade,
max_grade: config.max_grade,
max_teamsize: if config.team_participation.unwrap_or(false) { Some(2) } else { None },
positionalnumber: config.position,
protected: config.protected.unwrap_or(false),
requires_login: config.requires_login,
requires_contest: config.requires_contest.map(|list| list.join(",")),
secret: config.secret,
message: config.message,
image: config.image,
language: config.language.clone(),
category: config.category,
standalone_task: None,
tags: config.tags.unwrap_or_else(Vec::new),
taskgroups: Vec::new() };
for (positionalnumber, (name, info)) in config.tasks?.into_iter().enumerate() {
if let serde_yaml::Value::String(name) = name {
let mut taskgroup = Taskgroup::new(name, Some(positionalnumber as i32));
match info {
serde_yaml::Value::String(taskdir) => {
let task = Task::new(taskdir, config.language.clone(), 3);
taskgroup.tasks.push(task);
}
serde_yaml::Value::Sequence(taskdirs) => {
let mut stars = 2;
for taskdir in taskdirs {
if let serde_yaml::Value::String(taskdir) = taskdir {
let task = Task::new(taskdir, config.language.clone(), stars);
taskgroup.tasks.push(task);
} else {
panic!("Invalid contest YAML: {}{} (a)", directory, filename)
}
stars += 1;
}
}
serde_yaml::Value::Mapping(taskdirs) => {
let mut stars = 2;
for (taskdir, taskinfo) in taskdirs {
if let (serde_yaml::Value::String(taskdir), serde_yaml::Value::Mapping(taskinfo)) =
(taskdir, taskinfo)
{
if let Some(serde_yaml::Value::Number(cstars)) =
taskinfo.get(&serde_yaml::Value::String("stars".to_string()))
{
stars = cstars.as_u64().unwrap() as i32;
}
let task = Task::new(taskdir, config.language.clone(), stars);
taskgroup.tasks.push(task);
stars += 1;
} else {
panic!("Invalid contest YAML: {}{} (b)", directory, filename)
}
}
}
_ => panic!("Invalid contest YAML: {}{} (c)", directory, filename),
}
contest.taskgroups.push(taskgroup);
} else {
panic!("Invalid contest YAML: {}{} (d)", directory, filename)
}
}
Some(vec![contest])
}
#[derive(Debug)]
enum ConfigError {
#[allow(dead_code)]
ParseError(serde_yaml::Error),
MissingField,
}
fn parse_task_yaml(content: &str, filename: &str, directory: &str) -> Result<Vec<Contest>, ConfigError> {
let config: TaskYaml = serde_yaml::from_str(&content).map_err(ConfigError::ParseError)?;
if config.standalone != Some(true) {
return Err(ConfigError::MissingField);
}
let languages = config.languages.ok_or(ConfigError::MissingField)?;
if languages.len() == 0 {
return Err(ConfigError::MissingField);
}
let mut contests = Vec::new();
for language in languages {
let name = config.name.clone().unwrap_or_else(|| panic!("'name' missing in {}{}", directory, filename));
let mut contest = Contest { id: None,
location: directory.to_string(),
filename: format!("{}_{}", language, filename),
name: name.clone(),
duration: 0,
public: config.public_listing.unwrap_or(false),
start: None,
end: None,
review_start: None,
review_end: None,
min_grade: None,
max_grade: None,
max_teamsize: None,
positionalnumber: config.position,
protected: false,
requires_login: Some(false),
requires_contest: None,
secret: None,
message: None,
image: config.image.clone(),
language: Some(language.clone()),
category: Some("standalone_task".to_string()),
standalone_task: Some(true),
tags: config.tags.clone().unwrap_or_else(Vec::new),
taskgroups: Vec::new() };
let mut taskgroup = Taskgroup::new(name, None);
let stars = 0;
let taskdir = ".".to_string();
let task = Task::new(taskdir, Some(language), stars);
taskgroup.tasks.push(task);
contest.taskgroups.push(taskgroup);
contests.push(contest);
}
Ok(contests)
}
fn read_task_or_contest(p: &Path) -> Option<Vec<Contest>> {
use std::fs::File;
use std::io::Read;
let mut file = File::open(p).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).ok()?;
let filename: &str = p.file_name().to_owned()?.to_str()?;
if filename == "task.yaml" {
parse_task_yaml(&contents, filename, &format!("{}/", p.parent().unwrap().to_str()?)).ok()
} else {
parse_contest_yaml(&contents, filename, &format!("{}/", p.parent().unwrap().to_str()?))
}
}
use config::Config;
pub fn get_all_contest_info(task_dir: &str, config: &Config) -> Vec<Contest> {
fn walk_me_recursively(p: &Path, contests: &mut Vec<Contest>, config: &Config) {
if let Ok(paths) = std::fs::read_dir(p) {
let mut paths: Vec<_> = paths.filter_map(|r| r.ok()).collect();
paths.sort_by_key(|dir| dir.path());
for path in paths {
let p = path.path();
walk_me_recursively(&p, contests, config);
}
}
let filename = p.file_name().unwrap().to_string_lossy().to_string();
if filename.ends_with(".yaml") {
{
use std::io::Write;
print!(".");
std::io::stdout().flush().unwrap();
}
let mut restricted = false;
config.restricted_task_directories.as_ref().map(|restricted_task_directories| {
let pathname = p.to_string_lossy().to_string();
restricted_task_directories.iter().for_each(|restricted_task_directory| {
if pathname.starts_with(restricted_task_directory) {
restricted = true;
}
});
});
if let Some(cs) = read_task_or_contest(p) {
for c in cs {
if restricted {
if c.public {
println!("\nWARNING: Skipping public contest defined in '{}' due to being in a restricted directory!", p.display());
continue;
}
if c.secret.is_none() {
println!("\nWARNING: Contest defined in '{}' has no secret, can only be reached via id!",
p.display());
}
}
contests.push(c);
}
}
};
}
let mut contests = Vec::new();
match std::fs::read_dir(task_dir) {
Err(why) => eprintln!("Error opening tasks directory! {:?}", why.kind()),
Ok(paths) => {
for path in paths {
walk_me_recursively(&path.unwrap().path(), &mut contests, config);
}
}
};
contests
}
#[test]
fn parse_contest_yaml_no_tasks() {
let contest_file_contents = r#"
name: "JwInf 2020 Runde 1: Jgst. 3 – 6"
duration_minutes: 60
"#;
let contest = parse_contest_yaml(contest_file_contents, "", "");
assert!(contest.is_none());
}
#[test]
fn parse_contest_yaml_dates() {
let contest_file_contents = r#"
name: "JwInf 2020 Runde 1: Jgst. 3 – 6"
participation_start: "2022-03-01T00:00:00+01:00"
participation_end: "2022-03-31T22:59:59+01:00"
duration_minutes: 60
tasks: {}
"#;
let contest = parse_contest_yaml(contest_file_contents, "", "");
assert!(contest.is_some());
}