1use crate::error::{DogeError, DogeResult};
6use crate::value::Value;
7
8fn a_class(name: &str) -> String {
11 let article = match name.chars().next() {
12 Some('A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u') => "an",
13 _ => "a",
14 };
15 format!("{article} {name}")
16}
17
18pub fn attr_get(obj: &Value, name: &str) -> DogeResult {
23 match obj {
24 Value::Object(o) => {
25 let data = o.borrow();
26 data.fields.get(name).cloned().ok_or_else(|| {
27 DogeError::attr_error(format!("{} has no field {name}", a_class(&data.class_name)))
28 })
29 }
30 Value::Error(e) => crate::error::error_field(e, name),
31 _ => Err(DogeError::type_error(format!(
32 "cannot read the field {name} of {}",
33 obj.describe()
34 ))),
35 }
36}
37
38pub fn attr_get_or_bind(
47 obj: &Value,
48 name: &str,
49 class_has_method: &dyn Fn(u32, &str) -> bool,
50) -> DogeResult {
51 match obj {
52 Value::Object(o) => {
53 let data = o.borrow();
54 if let Some(value) = data.fields.get(name) {
55 return Ok(value.clone());
56 }
57 if class_has_method(data.class_id, name) {
58 return Ok(Value::bound_method(obj.clone(), name));
59 }
60 Err(DogeError::attr_error(format!(
61 "{} has no field or method {name}",
62 a_class(&data.class_name)
63 )))
64 }
65 Value::List(_) | Value::Dict(_) if crate::methods::has_builtin_method(obj, name) => {
66 Ok(Value::bound_method(obj.clone(), name))
67 }
68 _ => attr_get(obj, name),
69 }
70}
71
72pub fn attr_set(obj: &Value, name: &str, value: Value) -> DogeResult<()> {
75 match obj {
76 Value::Object(o) => {
77 o.borrow_mut().fields.insert(name.to_string(), value);
78 Ok(())
79 }
80 _ => Err(DogeError::type_error(format!(
81 "cannot set the field {name} on {}",
82 obj.describe()
83 ))),
84 }
85}
86
87pub fn object_class_id(recv: &Value) -> DogeResult<u32> {
91 match recv {
92 Value::Object(o) => Ok(o.borrow().class_id),
93 _ => Err(no_methods_error(recv)),
94 }
95}
96
97pub fn no_such_method(recv: &Value, method: &str) -> DogeError {
101 match recv {
102 Value::Object(o) => {
103 let data = o.borrow();
104 DogeError::attr_error(format!(
105 "{} has no method {method}",
106 a_class(&data.class_name)
107 ))
108 }
109 _ => no_methods_error(recv),
110 }
111}
112
113pub fn no_methods_error(recv: &Value) -> DogeError {
117 DogeError::attr_error(format!("{} has no methods", recv.describe()))
118}
119
120pub fn method_arity_error(
124 class: &str,
125 method: &str,
126 min: usize,
127 max: Option<usize>,
128 got: usize,
129) -> DogeError {
130 DogeError::type_error(crate::functions::arity_phrase(
131 &format!("{class}.{method}"),
132 min,
133 max,
134 got,
135 ))
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::error::ErrorKind;
142
143 #[test]
144 fn set_then_get_round_trips_a_field() {
145 let obj = Value::object(0, "Shibe");
146 attr_set(&obj, "name", Value::str("kabosu")).unwrap();
147 assert!(matches!(attr_get(&obj, "name").unwrap(), Value::Str(s) if &*s == "kabosu"));
148 }
149
150 #[test]
151 fn binds_a_method_when_the_class_defines_it() {
152 let obj = Value::object(0, "Shibe");
153 let bound = attr_get_or_bind(&obj, "speak", &|cid, name| cid == 0 && name == "speak")
154 .expect("speak binds");
155 match bound {
156 Value::BoundMethod(m) => {
157 assert_eq!(&*m.method, "speak");
158 assert!(matches!(&m.receiver, Value::Object(_)));
159 }
160 other => panic!("expected a bound method, got {}", other.type_name()),
161 }
162 }
163
164 #[test]
165 fn a_field_wins_over_a_method_of_the_same_name() {
166 let obj = Value::object(0, "Shibe");
167 attr_set(&obj, "speak", Value::int(1)).unwrap();
168 let got = attr_get_or_bind(&obj, "speak", &|_, _| true).unwrap();
170 assert!(crate::values_equal(&got, &Value::int(1)));
171 }
172
173 #[test]
174 fn neither_field_nor_method_is_a_catchable_attr_error() {
175 let obj = Value::object(0, "Shibe");
176 let err = attr_get_or_bind(&obj, "fly", &|_, _| false).unwrap_err();
177 assert_eq!(err.kind, ErrorKind::AttrError);
178 assert_eq!(err.message, "a Shibe has no field or method fly");
179 }
180
181 #[test]
182 fn binds_a_collection_method() {
183 let list = Value::list(vec![]);
184 let bound = attr_get_or_bind(&list, "append", &|_, _| false).expect("append binds");
185 assert!(matches!(bound, Value::BoundMethod(_)));
186 let err = attr_get_or_bind(&list, "nope", &|_, _| false).unwrap_err();
188 assert_eq!(err.kind, ErrorKind::TypeError);
189 }
190
191 #[test]
192 fn missing_field_is_a_catchable_attr_error() {
193 let obj = Value::object(0, "Shibe");
194 let err = attr_get(&obj, "tail").unwrap_err();
195 assert_eq!(err.kind, ErrorKind::AttrError);
196 assert_eq!(err.message, "a Shibe has no field tail");
197 }
198
199 #[test]
200 fn attr_on_a_non_object_is_a_type_error() {
201 let err = attr_get(&Value::int(1), "name").unwrap_err();
202 assert_eq!(err.kind, ErrorKind::TypeError);
203 let err = attr_set(&Value::int(1), "name", Value::None).unwrap_err();
204 assert_eq!(err.kind, ErrorKind::TypeError);
205 }
206
207 #[test]
208 fn class_id_reads_the_object_and_rejects_others() {
209 assert_eq!(object_class_id(&Value::object(3, "Shibe")).unwrap(), 3);
210 let err = object_class_id(&Value::int(1)).unwrap_err();
211 assert_eq!(err.kind, ErrorKind::AttrError);
212 }
213
214 #[test]
215 fn no_such_method_names_the_class() {
216 let err = no_such_method(&Value::object(0, "Shibe"), "fly");
217 assert_eq!(err.kind, ErrorKind::AttrError);
218 assert_eq!(err.message, "a Shibe has no method fly");
219 }
220
221 #[test]
222 fn no_methods_error_names_the_type_with_its_article() {
223 let err = no_methods_error(&Value::int(1));
224 assert_eq!(err.kind, ErrorKind::AttrError);
225 assert_eq!(err.message, "an Int has no methods");
226 assert_eq!(
227 no_methods_error(&Value::str("x")).message,
228 "a Str has no methods"
229 );
230 }
231
232 #[test]
233 fn method_arity_error_matches_the_user_wording() {
234 assert_eq!(
235 method_arity_error("Shibe", "init", 2, Some(2), 1).message,
236 "Shibe.init takes 2 arguments, got 1"
237 );
238 assert_eq!(
239 method_arity_error("Shibe", "speak", 1, Some(1), 0).message,
240 "Shibe.speak takes 1 argument, got 0"
241 );
242 assert_eq!(
243 method_arity_error("Shibe", "greet", 1, None, 3).message,
244 "Shibe.greet takes at least 1 argument, got 3"
245 );
246 }
247}