use std::fs;
use std::path::Path;
use rdice_core::{DiceEngine, FaceValue};
use serde::Deserialize;
use crate::error::{CliError, Result};
const LEGACY_CUSTOM_PREFIX: &str = "\u{273d}";
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigFile {
#[serde(default)]
dice: Vec<ConfigDie>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigDie {
name: String,
faces: Vec<ConfigFace>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ConfigFace {
Integer(i64),
Text(String),
}
pub fn load_custom_dice(path: &Path, engine: &mut DiceEngine) -> Result<()> {
if !path.exists() {
return Ok(());
}
let contents = fs::read_to_string(path)?;
let config: ConfigFile = toml::from_str(&contents)?;
for die in config.dice {
let name = die
.name
.strip_prefix(LEGACY_CUSTOM_PREFIX)
.unwrap_or(&die.name);
validate_cli_die_name(name)?;
engine.create_die(name, die.faces.into_iter().map(Into::into).collect())?;
}
Ok(())
}
fn validate_cli_die_name(name: &str) -> Result<()> {
let reason = if name.parse::<i64>().is_ok() {
Some("integer tokens are modifiers")
} else if name.starts_with(|character: char| character.is_ascii_digit()) {
Some("a leading digit is parsed as a roll count")
} else if name.starts_with('-') {
Some("a leading '-' is parsed as an option")
} else {
None
};
if let Some(reason) = reason {
return Err(CliError::InvalidConfigDieName {
name: name.to_string(),
reason,
});
}
Ok(())
}
impl From<ConfigFace> for FaceValue {
fn from(face: ConfigFace) -> Self {
match face {
ConfigFace::Integer(value) => Self::Integer(value),
ConfigFace::Text(value) => Self::Text(value),
}
}
}