use std::fmt::Display;
use replique::dialogue::{Value, ValueType};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ValueError {
#[error("missing")]
Missing,
#[error("expected {expected}, got {got}")]
Type {
expected: &'static str,
got: ValueType,
},
#[error("`{got}` is not a `{expected}`")]
Unknown { expected: &'static str, got: String },
#[error("key `{key}`: {source}")]
Key {
key: String,
source: Box<ValueError>,
},
}
impl ValueError {
pub fn under_key(self, key: impl Into<String>) -> Self {
Self::Key {
key: key.into(),
source: Box::new(self),
}
}
pub fn wrong_type(expected: &'static str, value: &Value) -> Self {
Self::Type {
expected,
got: ValueType::type_of(value),
}
}
}
#[derive(Error, Debug, PartialEq)]
pub enum DialogueArgsError {
#[error("expected at most {expected} argument(s), got {got}")]
TooManyArgs { expected: usize, got: usize },
#[error("argument {index}: {source}")]
Argument { index: usize, source: ValueError },
}
#[derive(Debug, Clone, PartialEq)]
pub struct DialogueArgs(pub Vec<Value>);
pub trait FromValue: Sized {
fn from_value(value: Value) -> Result<Self, ValueError>;
}
impl FromValue for Value {
fn from_value(value: Value) -> Result<Self, ValueError> {
Ok(value)
}
}
impl FromValue for String {
fn from_value(value: Value) -> Result<Self, ValueError> {
match value {
Value::String(s) => Ok(s),
other => Err(ValueError::wrong_type("string", &other)),
}
}
}
impl FromValue for bool {
fn from_value(value: Value) -> Result<Self, ValueError> {
match value {
Value::Bool(b) => Ok(b),
other => Err(ValueError::wrong_type("bool", &other)),
}
}
}
pub trait FromMaybeValue: Sized {
fn from_maybe_value(value: Option<Value>) -> Result<Self, ValueError>;
}
impl<T: FromValue> FromMaybeValue for T {
fn from_maybe_value(value: Option<Value>) -> Result<Self, ValueError> {
T::from_value(value.ok_or(ValueError::Missing)?)
}
}
impl<T: FromValue> FromMaybeValue for Option<T> {
fn from_maybe_value(value: Option<Value>) -> Result<Self, ValueError> {
match value {
None => Ok(None),
Some(value) => T::from_value(value).map(Some),
}
}
}
macro_rules! impl_from_value_float {
($($T:ty),*) => {$(
impl FromValue for $T {
fn from_value(value: Value) -> Result<Self, ValueError> {
match value {
Value::Float(f) => Ok(f as $T),
other => Err(ValueError::wrong_type("float", &other)),
}
}
}
)*};
}
impl_from_value_float!(f32, f64);
macro_rules! impl_from_value_int {
($($T:ty),*) => {$(
impl FromValue for $T {
fn from_value(value: Value) -> Result<Self, ValueError> {
match value {
Value::Int(i) => Ok(i as $T),
other => Err(ValueError::wrong_type("int", &other)),
}
}
}
)*};
}
impl_from_value_int!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
pub trait FromDialogueArgs: Sized {
fn from_dialogue_args(args: DialogueArgs) -> Result<Self, DialogueArgsError>;
}
impl FromDialogueArgs for DialogueArgs {
fn from_dialogue_args(args: DialogueArgs) -> Result<Self, DialogueArgsError> {
Ok(args)
}
}
impl FromDialogueArgs for () {
fn from_dialogue_args(args: DialogueArgs) -> Result<Self, DialogueArgsError> {
match args.0.len() {
0 => Ok(()),
got => Err(DialogueArgsError::TooManyArgs { expected: 0, got }),
}
}
}
macro_rules! impl_from_dialogue_args {
($($T:ident),+) => {
impl<$($T: FromMaybeValue),+> FromDialogueArgs for ($($T,)+) {
#[allow(unused_assignments, non_snake_case)]
fn from_dialogue_args(args: DialogueArgs) -> Result<Self, DialogueArgsError> {
let expected = [$(stringify!($T)),+].len();
if args.0.len() > expected {
return Err(DialogueArgsError::TooManyArgs { expected, got: args.0.len() });
}
let mut values = args.0.into_iter();
let mut index = 0;
$(
let $T = $T::from_maybe_value(values.next()).map_err(
|source| DialogueArgsError::Argument { index, source },
)?;
index += 1;
)+
Ok(($($T,)+))
}
}
};
}
impl_from_dialogue_args!(A);
impl_from_dialogue_args!(A, B);
impl_from_dialogue_args!(A, B, C);
impl_from_dialogue_args!(A, B, C, D);
impl_from_dialogue_args!(A, B, C, D, E);
impl_from_dialogue_args!(A, B, C, D, E, F);
impl_from_dialogue_args!(A, B, C, D, E, F, G);
impl_from_dialogue_args!(A, B, C, D, E, F, G, H);
macro_rules! impl_from_dialogue_args_single {
($($T:ty),*) => {$(
impl FromDialogueArgs for $T {
fn from_dialogue_args(args: DialogueArgs) -> Result<Self, DialogueArgsError> {
<($T,)>::from_dialogue_args(args).map(|(value,)| value)
}
}
)*};
}
impl_from_dialogue_args_single!(
Value, String, bool, f32, f64, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize
);
pub trait IntoValue {
fn into_value(self) -> Value;
}
impl IntoValue for bool {
fn into_value(self) -> Value {
Value::Bool(self)
}
}
impl IntoValue for String {
fn into_value(self) -> Value {
Value::String(self)
}
}
impl IntoValue for Value {
fn into_value(self) -> Value {
self
}
}
impl IntoValue for std::collections::HashMap<String, Value> {
fn into_value(self) -> Value {
Value::Dict(self)
}
}
macro_rules! impl_from_value_float {
($($T:ty),*) => {$(
impl IntoValue for $T {
fn into_value(self) -> Value {
Value::Float(self as f64)
}
}
)*};
}
impl_from_value_float!(f32, f64);
macro_rules! impl_from_value_int {
($($T:ty),*) => {$(
impl IntoValue for $T {
fn into_value(self) -> Value {
Value::Int(self as i64)
}
}
)*};
}
impl_from_value_int!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
pub trait IntoFunctionOutput<M> {
fn into_function_output(self) -> Result<Value, String>;
}
pub struct PlainOutput;
pub struct ResultOutput;
impl<T: IntoValue> IntoFunctionOutput<PlainOutput> for T {
fn into_function_output(self) -> Result<Value, String> {
Ok(self.into_value())
}
}
impl<T: IntoValue, E: Display> IntoFunctionOutput<ResultOutput> for Result<T, E> {
fn into_function_output(self) -> Result<Value, String> {
self.map(IntoValue::into_value)
.map_err(|err| err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(args: Vec<Value>) -> DialogueArgs {
DialogueArgs(args)
}
fn str(text: &str) -> Value {
Value::String(text.to_owned())
}
#[test]
fn a_tuple_reads_one_argument_per_field() {
assert_eq!(
<(String, f32, bool)>::from_dialogue_args(args(vec![
str("bell"),
Value::Float(0.5),
Value::Bool(true),
])),
Ok(("bell".to_owned(), 0.5, true)),
);
}
#[test]
fn a_single_argument_needs_no_tuple() {
assert_eq!(
String::from_dialogue_args(args(vec![str("hi")])),
Ok("hi".to_owned())
);
assert_eq!(
bool::from_dialogue_args(args(vec![Value::Bool(true)])),
Ok(true)
);
}
#[test]
fn the_empty_tuple_takes_no_argument_at_all() {
assert_eq!(<()>::from_dialogue_args(args(vec![])), Ok(()));
assert_eq!(
<()>::from_dialogue_args(args(vec![Value::Bool(true)])),
Err(DialogueArgsError::TooManyArgs {
expected: 0,
got: 1
}),
);
}
#[test]
fn a_trailing_option_makes_an_argument_optional() {
assert_eq!(
<(String, Option<f32>)>::from_dialogue_args(args(vec![str("a")])),
Ok(("a".to_owned(), None)),
);
assert_eq!(
<(String, Option<f32>)>::from_dialogue_args(args(vec![str("b"), Value::Float(1.0)])),
Ok(("b".to_owned(), Some(1.0))),
);
assert_eq!(
<(String, Option<f32>)>::from_dialogue_args(args(vec![str("c"), Value::Bool(true)]))
.unwrap_err()
.to_string(),
"argument 1: expected float, got bool",
);
}
#[test]
fn a_number_keeps_the_type_it_was_written_with() {
assert_eq!(f32::from_value(Value::Float(3.0)), Ok(3.0));
assert_eq!(i64::from_value(Value::Int(3)), Ok(3));
assert_eq!(
f32::from_value(Value::Int(3)),
Err(ValueError::Type {
expected: "float",
got: ValueType::Int,
}),
);
assert_eq!(
i64::from_value(Value::Float(3.0)),
Err(ValueError::Type {
expected: "int",
got: ValueType::Float,
}),
);
}
#[test]
fn a_raw_value_passes_through_untouched() {
assert_eq!(Value::from_value(str("a")), Ok(str("a")));
let call = args(vec![str("a"), Value::Int(1)]);
assert_eq!(DialogueArgs::from_dialogue_args(call.clone()), Ok(call));
}
#[test]
fn a_type_of_your_own_composes_into_a_tuple() {
#[derive(Debug, PartialEq)]
struct Direction(String);
impl FromValue for Direction {
fn from_value(value: Value) -> Result<Self, ValueError> {
let word = String::from_value(value)?;
match word.as_str() {
"left" | "right" => Ok(Self(word)),
_ => Err(ValueError::Unknown {
expected: "Direction",
got: word,
}),
}
}
}
assert_eq!(
<(Direction, Option<f32>)>::from_dialogue_args(args(vec![str("left")])),
Ok((Direction("left".to_owned()), None)),
);
assert_eq!(
<(Direction,)>::from_dialogue_args(args(vec![str("up")]))
.unwrap_err()
.to_string(),
"argument 0: `up` is not a `Direction`",
);
}
#[test]
fn error_on_an_argument_that_is_missing_or_of_the_wrong_type() {
assert_eq!(
<(String, f32)>::from_dialogue_args(args(vec![str("a")])),
Err(DialogueArgsError::Argument {
index: 1,
source: ValueError::Missing,
}),
);
assert_eq!(
<(String,)>::from_dialogue_args(args(vec![Value::Bool(true)]))
.unwrap_err()
.to_string(),
"argument 0: expected string, got bool",
);
}
#[test]
fn an_error_under_a_key_names_the_key() {
#[derive(Debug, PartialEq)]
struct Stats {
hp: i64,
}
impl FromValue for Stats {
fn from_value(value: Value) -> Result<Self, ValueError> {
let Value::Dict(mut fields) = value else {
return Err(ValueError::wrong_type("dict", &value));
};
Ok(Self {
hp: i64::from_maybe_value(fields.remove("hp"))
.map_err(|err| err.under_key("hp"))?,
})
}
}
let dict = |value| Value::Dict(std::collections::HashMap::from([("hp".to_owned(), value)]));
assert_eq!(
Stats::from_value(dict(Value::Int(12))),
Ok(Stats { hp: 12 })
);
assert_eq!(
<(Stats,)>::from_dialogue_args(args(vec![dict(str("a"))]))
.unwrap_err()
.to_string(),
"argument 0: key `hp`: expected int, got string",
);
assert_eq!(
<(Stats,)>::from_dialogue_args(args(vec![Value::Dict(Default::default())]))
.unwrap_err()
.to_string(),
"argument 0: key `hp`: missing",
);
assert_eq!(
<(Stats,)>::from_dialogue_args(args(vec![Value::Int(1)]))
.unwrap_err()
.to_string(),
"argument 0: expected dict, got int",
);
}
#[test]
fn error_on_more_arguments_than_the_signature_holds() {
assert_eq!(
<(String,)>::from_dialogue_args(args(vec![str("a"), Value::Bool(true)])),
Err(DialogueArgsError::TooManyArgs {
expected: 1,
got: 2
}),
);
}
}