use crate::error::{DogeError, DogeResult};
use crate::value::Value;
fn a_class(name: &str) -> String {
let article = match name.chars().next() {
Some('A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u') => "an",
_ => "a",
};
format!("{article} {name}")
}
pub fn attr_get(obj: &Value, name: &str) -> DogeResult {
match obj {
Value::Object(o) => {
let data = o.borrow();
data.fields.get(name).cloned().ok_or_else(|| {
DogeError::attr_error(format!("{} has no field {name}", a_class(&data.class_name)))
})
}
Value::Error(e) => crate::error::error_field(e, name),
_ => Err(DogeError::type_error(format!(
"cannot read the field {name} of {}",
obj.describe()
))),
}
}
pub fn attr_get_or_bind(
obj: &Value,
name: &str,
class_has_method: &dyn Fn(u32, &str) -> bool,
) -> DogeResult {
match obj {
Value::Object(o) => {
let data = o.borrow();
if let Some(value) = data.fields.get(name) {
return Ok(value.clone());
}
if class_has_method(data.class_id, name) {
return Ok(Value::bound_method(obj.clone(), name));
}
Err(DogeError::attr_error(format!(
"{} has no field or method {name}",
a_class(&data.class_name)
)))
}
Value::List(_) | Value::Dict(_) if crate::methods::has_builtin_method(obj, name) => {
Ok(Value::bound_method(obj.clone(), name))
}
_ => attr_get(obj, name),
}
}
pub fn attr_set(obj: &Value, name: &str, value: Value) -> DogeResult<()> {
match obj {
Value::Object(o) => {
o.borrow_mut().fields.insert(name.to_string(), value);
Ok(())
}
_ => Err(DogeError::type_error(format!(
"cannot set the field {name} on {}",
obj.describe()
))),
}
}
pub fn object_class_id(recv: &Value) -> DogeResult<u32> {
match recv {
Value::Object(o) => Ok(o.borrow().class_id),
_ => Err(no_methods_error(recv)),
}
}
pub fn no_such_method(recv: &Value, method: &str) -> DogeError {
match recv {
Value::Object(o) => {
let data = o.borrow();
DogeError::attr_error(format!(
"{} has no method {method}",
a_class(&data.class_name)
))
}
_ => no_methods_error(recv),
}
}
pub fn no_methods_error(recv: &Value) -> DogeError {
DogeError::attr_error(format!("{} has no methods", recv.describe()))
}
pub fn method_arity_error(
class: &str,
method: &str,
min: usize,
max: Option<usize>,
got: usize,
) -> DogeError {
DogeError::type_error(crate::functions::arity_phrase(
&format!("{class}.{method}"),
min,
max,
got,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
#[test]
fn set_then_get_round_trips_a_field() {
let obj = Value::object(0, "Shibe");
attr_set(&obj, "name", Value::str("kabosu")).unwrap();
assert!(matches!(attr_get(&obj, "name").unwrap(), Value::Str(s) if &*s == "kabosu"));
}
#[test]
fn binds_a_method_when_the_class_defines_it() {
let obj = Value::object(0, "Shibe");
let bound = attr_get_or_bind(&obj, "speak", &|cid, name| cid == 0 && name == "speak")
.expect("speak binds");
match bound {
Value::BoundMethod(m) => {
assert_eq!(&*m.method, "speak");
assert!(matches!(&m.receiver, Value::Object(_)));
}
other => panic!("expected a bound method, got {}", other.type_name()),
}
}
#[test]
fn a_field_wins_over_a_method_of_the_same_name() {
let obj = Value::object(0, "Shibe");
attr_set(&obj, "speak", Value::int(1)).unwrap();
let got = attr_get_or_bind(&obj, "speak", &|_, _| true).unwrap();
assert!(crate::values_equal(&got, &Value::int(1)));
}
#[test]
fn neither_field_nor_method_is_a_catchable_attr_error() {
let obj = Value::object(0, "Shibe");
let err = attr_get_or_bind(&obj, "fly", &|_, _| false).unwrap_err();
assert_eq!(err.kind, ErrorKind::AttrError);
assert_eq!(err.message, "a Shibe has no field or method fly");
}
#[test]
fn binds_a_collection_method() {
let list = Value::list(vec![]);
let bound = attr_get_or_bind(&list, "append", &|_, _| false).expect("append binds");
assert!(matches!(bound, Value::BoundMethod(_)));
let err = attr_get_or_bind(&list, "nope", &|_, _| false).unwrap_err();
assert_eq!(err.kind, ErrorKind::TypeError);
}
#[test]
fn missing_field_is_a_catchable_attr_error() {
let obj = Value::object(0, "Shibe");
let err = attr_get(&obj, "tail").unwrap_err();
assert_eq!(err.kind, ErrorKind::AttrError);
assert_eq!(err.message, "a Shibe has no field tail");
}
#[test]
fn attr_on_a_non_object_is_a_type_error() {
let err = attr_get(&Value::int(1), "name").unwrap_err();
assert_eq!(err.kind, ErrorKind::TypeError);
let err = attr_set(&Value::int(1), "name", Value::None).unwrap_err();
assert_eq!(err.kind, ErrorKind::TypeError);
}
#[test]
fn class_id_reads_the_object_and_rejects_others() {
assert_eq!(object_class_id(&Value::object(3, "Shibe")).unwrap(), 3);
let err = object_class_id(&Value::int(1)).unwrap_err();
assert_eq!(err.kind, ErrorKind::AttrError);
}
#[test]
fn no_such_method_names_the_class() {
let err = no_such_method(&Value::object(0, "Shibe"), "fly");
assert_eq!(err.kind, ErrorKind::AttrError);
assert_eq!(err.message, "a Shibe has no method fly");
}
#[test]
fn no_methods_error_names_the_type_with_its_article() {
let err = no_methods_error(&Value::int(1));
assert_eq!(err.kind, ErrorKind::AttrError);
assert_eq!(err.message, "an Int has no methods");
assert_eq!(
no_methods_error(&Value::str("x")).message,
"a Str has no methods"
);
}
#[test]
fn method_arity_error_matches_the_user_wording() {
assert_eq!(
method_arity_error("Shibe", "init", 2, Some(2), 1).message,
"Shibe.init takes 2 arguments, got 1"
);
assert_eq!(
method_arity_error("Shibe", "speak", 1, Some(1), 0).message,
"Shibe.speak takes 1 argument, got 0"
);
assert_eq!(
method_arity_error("Shibe", "greet", 1, None, 3).message,
"Shibe.greet takes at least 1 argument, got 3"
);
}
}