Skip to content

Commit

Permalink
Added feature: dot operator support
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Wei committed Sep 1, 2023
1 parent 3e82046 commit 5552aeb
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/operator/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::vec;

use crate::function::builtin::builtin_function;

use crate::{context::Context, error::*, value::Value, ContextWithMutableVariables};
Expand Down Expand Up @@ -446,6 +448,25 @@ impl Operator {
VariableIdentifierRead { identifier } => {
expect_operator_argument_amount(arguments.len(), 0)?;

// object.attribute
if is_valid_start(identifier) {
if let Some((id, attribute)) = identifier.split_once('.') {
if is_valid_start(id) && is_valid_start(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 {
Expand All @@ -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_valid_start(id) && is_valid_start(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() =>
Expand Down Expand Up @@ -524,3 +562,17 @@ impl Operator {
}
}
}

fn is_valid_start(id: &str) -> bool {
match id.chars().next() {
Some(c) => c.is_ascii_alphabetic(),
None => false,
}
}

#[test]
fn test_is_valid_identifier() {
assert!(is_valid_start("abc123"));
assert!(!is_valid_start("123abc"));
assert!(!is_valid_start(""));
}
149 changes: 149 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,155 @@ 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()))
);

assert_eq!(
eval_with_context_mut("v.100", &mut context),
Err(EvalexprError::VariableIdentifierNotFound("v.100".into()))
);

assert_eq!(
eval_with_context_mut("100.v100", &mut context),
Err(EvalexprError::VariableIdentifierNotFound("100.v100".into()))
);

assert_eq!(
eval_with_context_mut("abc.efg", &mut context),
Err(EvalexprError::VariableIdentifierNotFound("abc".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();
Expand Down

0 comments on commit 5552aeb

Please sign in to comment.