diff --git a/src/operator/mod.rs b/src/operator/mod.rs index 386357a..6bb99f2 100644 --- a/src/operator/mod.rs +++ b/src/operator/mod.rs @@ -1,3 +1,5 @@ +use std::vec; + use crate::function::builtin::builtin_function; use crate::{context::Context, error::*, value::Value, ContextWithMutableVariables}; @@ -446,6 +448,25 @@ impl Operator { VariableIdentifierRead { identifier } => { expect_operator_argument_amount(arguments.len(), 0)?; + // object.attribute + if is_identifier(identifier) { + if let Some((id, attribute)) = identifier.split_once('.') { + if is_identifier(id) && is_identifier(attribute) { + let object = match context.get_value(id) { + Some(x) => Ok(x), + None => Err(EvalexprError::VariableIdentifierNotFound(id.into())), + }?; + let params = vec![ + object.clone(), + Value::String(attribute.into()), + Value::Empty, + ]; + // dot(object, "attribute", empty) + return context.call_function("dot", &Value::Tuple(params)); + } + } + } + if let Some(value) = context.get_value(identifier).cloned() { Ok(value) } else { @@ -458,6 +479,23 @@ impl Operator { expect_operator_argument_amount(arguments.len(), 1)?; let arguments = &arguments[0]; + // object.method() + if let Some((id, method)) = identifier.split_once('.') { + if is_identifier(id) && is_identifier(method) { + let object = match context.get_value(id) { + Some(x) => Ok(x), + None => Err(EvalexprError::VariableIdentifierNotFound(id.into())), + }?; + let params = vec![ + object.clone(), + Value::String(method.into()), + arguments.clone(), + ]; + // dot(object, "method", arguments) + return context.call_function("dot", &Value::Tuple(params)); + } + } + match context.call_function(identifier, arguments) { Err(EvalexprError::FunctionIdentifierNotFound(_)) if !context.are_builtin_functions_disabled() => @@ -524,3 +562,10 @@ impl Operator { } } } + +fn is_identifier(id: &str) -> bool { + match id.chars().next() { + Some(c) => c.is_ascii_alphabetic(), + None => false, + } +} diff --git a/tests/integration.rs b/tests/integration.rs index df65245..a2a282f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -142,6 +142,140 @@ fn test_with_context() { ); } +#[test] +fn test_dot_attribute() { + let mut context = HashMapContext::new(); + + context + .set_function( + "array".to_string(), + Function::new(|argument| Ok(Value::Tuple(argument.as_tuple()?))), + ) + .unwrap(); + + context + .set_function( + "dot".to_string(), + Function::new(move |argument| { + let x = argument.as_fixed_len_tuple(3)?; + if let (Value::Tuple(id), Value::String(method)) = (&x[0], &x[1]) { + match method.as_str() { + "push" => { + // array.push(x) + let mut array = id.clone(); + array.push(x[2].clone()); + return Ok(Value::Tuple(array)); + }, + "get" => { + // array.get(i) + let index = x[2].as_int()?; + let value = &id[index as usize]; + return Ok(value.clone()); + }, + "length" => { + // array.length + return Ok(Value::Int(id.len() as i64)); + }, + _ => {}, + } + } + Err(EvalexprError::CustomMessage("unexpected dot call".into())) + }), + ) + .unwrap(); + + assert_eq!( + eval_with_context_mut( + "v = array(1,2,3,4,5); + v = v.push(6); + v.length == v.get(5)", + &mut context + ), + Ok(Value::Boolean(true)) + ); + + assert_eq!( + eval_with_context_mut("v.get(4)", &mut context), + Ok(Value::Int(5)) + ); + + // attribute is a method with empty input + assert_eq!( + eval_with_context_mut("v.length() == v.length", &mut context), + Ok(Value::Boolean(true)) + ); + + assert_eq!( + eval_with_context_mut("v.100(4)", &mut context), + Err(EvalexprError::FunctionIdentifierNotFound("v.100".into())) + ); +} + +#[test] +fn test_dot_function() { + let mut context = HashMapContext::new(); + context + .set_function( + "dot".to_string(), + Function::new(|argument| { + let x = argument.as_fixed_len_tuple(3)?; + if let (Value::Int(id), Value::String(method), args) = (&x[0], &x[1], &x[2]) { + if method == "add" { + let input = args.as_fixed_len_tuple(2)?; + if let (Value::Int(a), Value::Int(b)) = (&input[0], &input[1]) { + return Ok(Value::Int(id + a + b)); + } + } + } + Err(EvalexprError::CustomMessage("unexpected dot call".into())) + }), + ) + .unwrap(); + context + .set_value("object".to_string(), Value::Int(10)) + .unwrap(); + + // success + assert_eq!( + eval_with_context("object.add(5, 6)", &context), + Ok(Value::Int(21)) + ); + + assert_eq!( + eval_with_context_mut("alien = 100; alien.add(5, 6)", &mut context), + Ok(Value::Int(111)) + ); + + // empty + assert_eq!( + eval_with_context("object.add()", &context), + Err(EvalexprError::ExpectedTuple { + actual: Value::Empty + }) + ); + + // too many + assert_eq!( + eval_with_context("object.add(5, 6, 7)", &context), + Err(EvalexprError::ExpectedFixedLenTuple { + expected_len: 2, + actual: Value::Tuple(vec![Value::Int(5), Value::Int(6), Value::Int(7)]) + }) + ); + + // user dose not implement + assert_eq!( + eval_with_context("object.remove(5, 6)", &context), + Err(EvalexprError::CustomMessage("unexpected dot call".into())) + ); + + // no such identifier + assert_eq!( + eval_with_context("unkown.add(5)", &context), + Err(EvalexprError::VariableIdentifierNotFound("unkown".into())) + ); +} + #[test] fn test_functions() { let mut context = HashMapContext::new();