#![allow(clippy::manual_filter_map)]
use rand::Rng;
use thiserror::Error;
use serde_derive::Deserialize;
pub const CONF_LINE_SECRET_MODIFIER_VISIBLE: char = '_';
pub const CONF_LINE_SECRET_MODIFIER_LINEBREAK1: char = '\n';
pub const CONF_LINE_SECRET_MODIFIER_LINEBREAK2: char = '|';
#[derive(Error, Debug)]
pub enum ConfigParseError {
#[error(
"Syntax error in line {line_number:?}: `{line}`\n\n\
The game modifier must be one of the following:\n\
:traditional-rewarding\n\
:success-rewarding\n\n\
Edit config file and start again.\n"
)]
GameModifier { line_number: usize, line: String },
#[error(
"Syntax error in line {line_number:?}: `{line}`\n\n\
The first character of every non-empty line has to be one of the following:\n\
any letter or digit (secret string),\n\
'#' (comment line),\n\
'-' (secret string),\n\
'|' (ASCII-Art image) or\n\
':' (game modifier).\n\n\
Edit config file and start again.\n"
)]
LineIdentifier { line_number: usize, line: String },
#[error["No image data found."]]
NoImageData,
#[error["A config file must have a least one secret string, which is\n\
a non-empty line starting with a letter, digit, '_' or '-'."]]
NoSecretString,
#[error["Could not parse the proprietary format, because this is\n\
meant to be in (erroneous) YAML format."]]
NotInProprietaryFormat,
#[error(
"Syntax error: Please follow the example below.\n\
(The custom image is optional, it's lines start with a space.):\n\
\t------------------------------\n\
\tsecrets: \n\
\t- guess me\n\
\t- \"guess me: with colon\"\n\
\t- line| break\n\
\t- _disclose _partly\n\
\n\
\timage: |1\n\
\t :\n\
\t |_|>\n\
\t------------------------------\n\
{0}"
)]
NotInYamlFormat(#[from] serde_yaml::Error),
#[error["No line: `secrets:` found (no spaces allowed before)."]]
YamlSecretsLineMissing,
}
impl PartialEq for ConfigParseError {
fn eq(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
&& (self.to_string() == other.to_string())
}
}
#[derive(Debug, PartialEq, Deserialize)]
pub struct Dict {
secrets: Vec<String>,
}
impl Dict {
pub fn from(lines: &str) -> Result<Self, ConfigParseError> {
let lines = lines.trim_start_matches('\u{feff}');
if !lines
.lines()
.filter(|s| !s.trim_start().starts_with('#'))
.filter(|s| s.trim() != "")
.any(|s| s.trim_end() == "secrets:")
{
return Err(ConfigParseError::YamlSecretsLineMissing);
}
let dict: Dict = serde_yaml::from_str(lines)?;
Ok(dict)
}
pub fn get_random_secret(&mut self) -> Option<String> {
match self.secrets.len() {
0 => None,
1 => Some(self.secrets.swap_remove(0)),
_ => {
let mut rng = rand::thread_rng();
let i = rng.gen_range(0..self.secrets.len());
Some(self.secrets.swap_remove(i))
}
}
}
pub fn is_empty(&self) -> bool {
self.secrets.is_empty()
}
pub fn add(&mut self, secret: String) {
self.secrets.push(secret);
}
}
#[cfg(test)]
mod tests {
use super::ConfigParseError;
use super::Dict;
#[test]
fn test_from() {
let config: &str = "
# comment
secrets:
- guess me
- hang_man_
- _good l_uck
traditional: true
";
let dict = Dict::from(&config).unwrap();
let expected = Dict {
secrets: vec![
"guess me".to_string(),
"hang_man_".to_string(),
"_good l_uck".to_string(),
],
};
assert_eq!(dict, expected);
let config = "# comment\nsecrets:\n - guess me\n";
let dict = Dict::from(&config);
let expected = Ok(Dict {
secrets: vec!["guess me".to_string()],
});
assert_eq!(dict, expected);
let config = "# comment\nsecrets:\n- guess me\n";
let dict = Dict::from(&config);
let expected = Ok(Dict {
secrets: vec!["guess me".to_string()],
});
assert_eq!(dict, expected);
let config = "# comment\nsecrets:\n- 222\n";
let dict = Dict::from(&config);
let expected = Ok(Dict {
secrets: vec!["222".to_string()],
});
assert_eq!(dict, expected);
let config = "sxxxecrets:";
let dict = Dict::from(&config).unwrap_err();
assert!(matches!(dict, ConfigParseError::YamlSecretsLineMissing));
let config = "# comment\nsecrets:\n guess me\n";
let dict = Dict::from(&config).unwrap_err();
assert!(matches!(dict, ConfigParseError::NotInYamlFormat(_)));
}
}