mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 16:01:41 +00:00
0.3.0
This commit is contained in:
+50
-1
@@ -15,6 +15,7 @@ pub enum Value {
|
||||
NumberList(Rc<Vec<Decimal>>),
|
||||
StringList(Rc<Vec<SmolStr>>),
|
||||
Object(Rc<IndexMap<SmolStr, Value>>),
|
||||
List(Rc<Vec<Value>>),
|
||||
}
|
||||
|
||||
/// Type tag constants for serialization
|
||||
@@ -25,6 +26,7 @@ pub const TYPE_BOOLEAN: u8 = 0x03;
|
||||
pub const TYPE_NUMBER_LIST: u8 = 0x04;
|
||||
pub const TYPE_STRING_LIST: u8 = 0x05;
|
||||
pub const TYPE_OBJECT: u8 = 0x06;
|
||||
pub const TYPE_LIST: u8 = 0x07;
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -63,6 +65,16 @@ impl fmt::Display for Value {
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
Value::List(list) => {
|
||||
write!(f, "[")?;
|
||||
for (i, val) in list.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}", val)?;
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +90,7 @@ impl Value {
|
||||
Value::NumberList(_) => TYPE_NUMBER_LIST,
|
||||
Value::StringList(_) => TYPE_STRING_LIST,
|
||||
Value::Object(_) => TYPE_OBJECT,
|
||||
Value::List(_) => TYPE_LIST,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ impl Value {
|
||||
Value::NumberList(_) => "NumberList",
|
||||
Value::StringList(_) => "StringList",
|
||||
Value::Object(_) => "Object",
|
||||
Value::List(_) => "List",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +164,15 @@ impl Value {
|
||||
bytes.extend_from_slice(&val.serialize());
|
||||
}
|
||||
}
|
||||
Value::List(list) => {
|
||||
// List length (2 bytes)
|
||||
bytes.push((list.len() >> 8) as u8);
|
||||
bytes.push(list.len() as u8);
|
||||
// List items (recursive serialization)
|
||||
for val in list.iter() {
|
||||
bytes.extend_from_slice(&val.serialize());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
@@ -226,6 +249,12 @@ impl From<IndexMap<SmolStr, Value>> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Value>> for Value {
|
||||
fn from(v: Vec<Value>) -> Self {
|
||||
Value::List(Rc::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Deserialize a value from bytes
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<(Value, usize), String> {
|
||||
@@ -357,6 +386,22 @@ impl Value {
|
||||
|
||||
Ok((Value::Object(Rc::new(map)), pos))
|
||||
}
|
||||
TYPE_LIST => {
|
||||
if bytes.len() < pos + 2 {
|
||||
return Err("Insufficient bytes for List length".to_string());
|
||||
}
|
||||
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
|
||||
let mut list = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
let (val, val_bytes) = Value::deserialize(&bytes[pos..])?;
|
||||
pos += val_bytes;
|
||||
list.push(val);
|
||||
}
|
||||
|
||||
Ok((Value::List(Rc::new(list)), pos))
|
||||
}
|
||||
_ => Err(format!("Unknown type tag: {}", type_tag)),
|
||||
}
|
||||
}
|
||||
@@ -426,7 +471,11 @@ impl Value {
|
||||
.collect();
|
||||
Ok(Value::StringList(Rc::new(strings)))
|
||||
} else {
|
||||
Err("Arrays must contain all numbers or all strings".to_string())
|
||||
let mut items = Vec::with_capacity(arr.len());
|
||||
for item in arr {
|
||||
items.push(Self::from_json_value(item)?);
|
||||
}
|
||||
Ok(Value::List(Rc::new(items)))
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
|
||||
@@ -121,6 +121,19 @@ impl LanguageInfo {
|
||||
type_name: v.type_name().to_string(),
|
||||
}).collect())
|
||||
}
|
||||
Value::List(list) => {
|
||||
// For List of Objects, derive fields from the first Object element
|
||||
list.iter().find_map(|item| {
|
||||
if let Value::Object(map) = item {
|
||||
Some(map.iter().map(|(k, v)| FieldInfo {
|
||||
name: k.to_string(),
|
||||
type_name: v.type_name().to_string(),
|
||||
}).collect())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.variables.push(VariableInfo {
|
||||
@@ -276,6 +289,23 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
|
||||
MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") },
|
||||
MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") },
|
||||
]),
|
||||
("List", vec![
|
||||
MethodInfo { name: "length", signature: "() -> Number", doc: Some("Number of elements") },
|
||||
MethodInfo { name: "len", signature: "() -> Number", doc: None },
|
||||
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
|
||||
MethodInfo { name: "first", signature: "() -> any", doc: Some("First element") },
|
||||
MethodInfo { name: "last", signature: "() -> any", doc: Some("Last element") },
|
||||
MethodInfo { name: "get", signature: "(index: Number) -> any", doc: None },
|
||||
MethodInfo { name: "contains", signature: "(value: any) -> Boolean", doc: None },
|
||||
MethodInfo { name: "indexOf", signature: "(value: any) -> Number", doc: None },
|
||||
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> List", doc: None },
|
||||
MethodInfo { name: "reverse", signature: "() -> List", doc: None },
|
||||
MethodInfo { name: "join", signature: "(delim?: String) -> String", doc: None },
|
||||
MethodInfo { name: "map", signature: "(field: String) -> NumberList | StringList | List", doc: Some("Extract field from each Object element") },
|
||||
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
|
||||
MethodInfo { name: "find", signature: "(field: String, value?: any) -> any", doc: Some("Find first element matching field condition") },
|
||||
MethodInfo { name: "sort", signature: "(field: String) -> List", doc: Some("Sort by field value") },
|
||||
]),
|
||||
("StringList", vec![
|
||||
MethodInfo { name: "length", signature: "() -> Number", doc: None },
|
||||
MethodInfo { name: "len", signature: "() -> Number", doc: None },
|
||||
|
||||
@@ -160,6 +160,7 @@ impl<'a> VM<'a> {
|
||||
Value::StringList(l) => Decimal::from(l.len()),
|
||||
Value::NumberList(l) => Decimal::from(l.len()),
|
||||
Value::Object(m) => Decimal::from(m.len()),
|
||||
Value::List(l) => Decimal::from(l.len()),
|
||||
other => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"len() not supported for type {}",
|
||||
|
||||
+252
-3
@@ -26,6 +26,7 @@ impl<'a> VM<'a> {
|
||||
Value::StringList(_) => self.dispatch_string_list_method_inner(&obj_val, method, args),
|
||||
Value::NumberList(_) => self.dispatch_number_list_method_inner(&obj_val, method, args),
|
||||
Value::Object(_) => self.dispatch_object_method_inner(&obj_val, method, args),
|
||||
Value::List(_) => self.dispatch_list_method_inner(&obj_val, method, args),
|
||||
_ => {
|
||||
// Try external methods for any type
|
||||
let type_name: SmolStr = obj_val.type_name().into();
|
||||
@@ -452,9 +453,7 @@ impl<'a> VM<'a> {
|
||||
.collect();
|
||||
Ok(Value::NumberList(Rc::new(numbers)))
|
||||
} else {
|
||||
Err(VMError::RuntimeError(
|
||||
"values() only works when all values are the same type (String or Number)".to_string(),
|
||||
))
|
||||
Ok(Value::List(Rc::new(vals)))
|
||||
}
|
||||
}
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(map.len()))),
|
||||
@@ -497,4 +496,254 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_list_method_inner(
|
||||
&self,
|
||||
obj_val: &Value,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<Value, VMError> {
|
||||
let list = match obj_val {
|
||||
Value::List(l) => l,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match method {
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(list.len()))),
|
||||
"isEmpty" => Ok(Value::Boolean(list.is_empty())),
|
||||
"first" => Ok(list.first().cloned().unwrap_or(Value::Null)),
|
||||
"last" => Ok(list.last().cloned().unwrap_or(Value::Null)),
|
||||
"get" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("get() requires an index".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Number(idx) => {
|
||||
let index = idx.to_usize().unwrap_or(usize::MAX);
|
||||
Ok(list.get(index).cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"contains" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
|
||||
}
|
||||
Ok(Value::Boolean(list.contains(&args[0])))
|
||||
}
|
||||
"indexOf" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
|
||||
}
|
||||
let idx = list.iter().position(|item| item == &args[0]);
|
||||
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
|
||||
}
|
||||
"slice" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Number(start_idx) => {
|
||||
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
|
||||
let end = if args.len() > 1 {
|
||||
match &args[1] {
|
||||
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
|
||||
_ => list.len(),
|
||||
}
|
||||
} else {
|
||||
list.len()
|
||||
};
|
||||
Ok(Value::List(Rc::new(list[start..end].to_vec())))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"reverse" => {
|
||||
let mut reversed = list.to_vec();
|
||||
reversed.reverse();
|
||||
Ok(Value::List(Rc::new(reversed)))
|
||||
}
|
||||
"join" => {
|
||||
let delim = if args.is_empty() {
|
||||
""
|
||||
} else {
|
||||
match &args[0] {
|
||||
Value::String(s) => s.as_str(),
|
||||
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
|
||||
}
|
||||
};
|
||||
let strings: Vec<String> = list.iter().map(|v| format!("{}", v)).collect();
|
||||
Ok(Value::String(SmolStr::new(strings.join(delim))))
|
||||
}
|
||||
"map" => {
|
||||
// Property shorthand: list.map("fieldName") extracts a field from each Object element
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("map() requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
let mut values: Vec<Value> = Vec::with_capacity(list.len());
|
||||
for item in list.iter() {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
values.push(map.get(field.as_str()).cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"map() requires all elements to be Objects, got {}",
|
||||
item.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Smart return type: if all values are the same primitive type, return typed list
|
||||
if values.is_empty() {
|
||||
return Ok(Value::List(Rc::new(values)));
|
||||
}
|
||||
if values.iter().all(|v| matches!(v, Value::Number(_))) {
|
||||
let nums: Vec<Decimal> = values
|
||||
.into_iter()
|
||||
.map(|v| match v {
|
||||
Value::Number(n) => n,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
Ok(Value::NumberList(Rc::new(nums)))
|
||||
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
|
||||
let strings: Vec<SmolStr> = values
|
||||
.into_iter()
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
Ok(Value::StringList(Rc::new(strings)))
|
||||
} else {
|
||||
Ok(Value::List(Rc::new(values)))
|
||||
}
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("map() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
"filter" => {
|
||||
// Property shorthand:
|
||||
// list.filter("active") — filter by truthy boolean field
|
||||
// list.filter("field", value) — filter where field == value
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("filter() requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
let filtered: Result<Vec<Value>, VMError> = list
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
|
||||
if args.len() > 1 {
|
||||
// filter("field", value) — equality check
|
||||
if field_val == args[1] {
|
||||
Some(Ok(item.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// filter("field") — truthy check
|
||||
match &field_val {
|
||||
Value::Boolean(b) => if *b { Some(Ok(item.clone())) } else { None },
|
||||
Value::Null => None,
|
||||
_ => Some(Ok(item.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Some(Err(VMError::RuntimeError(format!(
|
||||
"filter() requires all elements to be Objects, got {}",
|
||||
item.type_name()
|
||||
)))),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Value::List(Rc::new(filtered?)))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("filter() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
"find" => {
|
||||
// Property shorthand: list.find("field", value) — first element where field == value
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("find() requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
for item in list.iter() {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
|
||||
if args.len() > 1 {
|
||||
if field_val == args[1] {
|
||||
return Ok(item.clone());
|
||||
}
|
||||
} else {
|
||||
// find("field") — first with truthy field
|
||||
match &field_val {
|
||||
Value::Boolean(b) => if *b { return Ok(item.clone()); },
|
||||
Value::Null => {},
|
||||
_ => return Ok(item.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"find() requires all elements to be Objects, got {}",
|
||||
item.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("find() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
"sort" => {
|
||||
// Property shorthand: list.sort("field") — sort by field value
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("sort() on List requires a field name argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(field) => {
|
||||
let mut sorted = list.to_vec();
|
||||
sorted.sort_by(|a, b| {
|
||||
let a_val = match a {
|
||||
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
|
||||
_ => Value::Null,
|
||||
};
|
||||
let b_val = match b {
|
||||
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
|
||||
_ => Value::Null,
|
||||
};
|
||||
match (&a_val, &b_val) {
|
||||
(Value::Number(a), Value::Number(b)) => a.cmp(b),
|
||||
(Value::String(a), Value::String(b)) => a.cmp(b),
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
}
|
||||
});
|
||||
Ok(Value::List(Rc::new(sorted)))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let key = (SmolStr::new_static("List"), SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
ext_method(obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: "List",
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +597,8 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
// String in Object (key check)
|
||||
(Value::String(key), Value::Object(map)) => map.contains_key(key),
|
||||
// Value in List
|
||||
(needle_val, Value::List(list)) => list.contains(needle_val),
|
||||
(needle_val, haystack_val) => {
|
||||
return Err(VMError::InvalidOperation {
|
||||
operation: "in",
|
||||
@@ -684,6 +686,42 @@ impl<'a> VM<'a> {
|
||||
let value = map.get(prop).cloned().unwrap_or(Value::Null);
|
||||
self.registers[dest] = value;
|
||||
}
|
||||
Value::List(list) => {
|
||||
// Property projection: list.field → extract field from each Object element
|
||||
let mut values: Vec<Value> = Vec::with_capacity(list.len());
|
||||
for item in list.iter() {
|
||||
match item {
|
||||
Value::Object(map) => {
|
||||
values.push(map.get(prop).cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"Cannot access property '{}' on non-Object element in List (got {})",
|
||||
prop,
|
||||
item.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Smart return type: NumberList/StringList when homogeneous
|
||||
if values.is_empty() {
|
||||
self.registers[dest] = Value::List(Rc::new(values));
|
||||
} else if values.iter().all(|v| matches!(v, Value::Number(_))) {
|
||||
let nums: Vec<Decimal> = values.into_iter().map(|v| match v {
|
||||
Value::Number(n) => n,
|
||||
_ => unreachable!(),
|
||||
}).collect();
|
||||
self.registers[dest] = Value::NumberList(Rc::new(nums));
|
||||
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
|
||||
let strings: Vec<SmolStr> = values.into_iter().map(|v| match v {
|
||||
Value::String(s) => s,
|
||||
_ => unreachable!(),
|
||||
}).collect();
|
||||
self.registers[dest] = Value::StringList(Rc::new(strings));
|
||||
} else {
|
||||
self.registers[dest] = Value::List(Rc::new(values));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"Cannot access property '{}' on type {}",
|
||||
|
||||
Reference in New Issue
Block a user