rdice-cli 0.2.0

Command-line dice roller powered by rdice-core
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),
        }
    }
}