use crate::expressions::tokenizer::{Token, TokenStream, check_redundant_parens, tokenize};
use crate::expressions::{
PathElement, TrackedExpressionAttributes, resolve_path, resolve_path_elements,
};
use crate::types::AttributeValue;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum ConditionExpr {
Comparison {
left: Operand,
op: CompOp,
right: Operand,
},
Between {
operand: Operand,
lo: Operand,
hi: Operand,
},
In {
operand: Operand,
values: Vec<Operand>,
},
AttributeExists(Vec<PathElement>),
AttributeNotExists(Vec<PathElement>),
AttributeType(Vec<PathElement>, Operand),
BeginsWith(Operand, Operand),
Contains(Operand, Operand),
And(Box<ConditionExpr>, Box<ConditionExpr>),
Or(Box<ConditionExpr>, Box<ConditionExpr>),
Not(Box<ConditionExpr>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Operand {
Path(Vec<PathElement>),
ValueRef(String),
Size(Vec<PathElement>),
}
pub fn parse(expr: &str) -> Result<ConditionExpr, String> {
let tokens = tokenize(expr).map_err(|e| e.to_string())?;
check_redundant_parens(&tokens)?;
let mut stream = TokenStream::new(tokens);
let result = parse_or(&mut stream)?;
if !stream.at_end() {
return Err(format!(
"Syntax error; token: \"{}\"",
stream.peek().unwrap()
));
}
Ok(result)
}
pub fn evaluate(
expr: &ConditionExpr,
item: &HashMap<String, AttributeValue>,
tracker: &TrackedExpressionAttributes,
) -> Result<bool, String> {
match expr {
ConditionExpr::Comparison { left, op, right } => {
let lv = resolve_operand(left, item, tracker)?;
let rv = resolve_operand(right, item, tracker)?;
match (lv, rv) {
(Some(l), Some(r)) => Ok(compare_values(&l, op, &r)),
_ => Ok(matches!(op, CompOp::Ne)),
}
}
ConditionExpr::Between { operand, lo, hi } => {
let val = resolve_operand(operand, item, tracker)?;
let lo_val = resolve_operand(lo, item, tracker)?;
let hi_val = resolve_operand(hi, item, tracker)?;
match (val, lo_val, hi_val) {
(Some(v), Some(l), Some(h)) => {
Ok(compare_values(&v, &CompOp::Ge, &l) && compare_values(&v, &CompOp::Le, &h))
}
_ => Ok(false),
}
}
ConditionExpr::In { operand, values } => {
let val = resolve_operand(operand, item, tracker)?;
match val {
Some(v) => {
for candidate in values {
let cv = resolve_operand(candidate, item, tracker)?;
if let Some(c) = cv {
if compare_values(&v, &CompOp::Eq, &c) {
return Ok(true);
}
}
}
Ok(false)
}
None => Ok(false),
}
}
ConditionExpr::AttributeExists(path) => {
let resolved = resolve_path_elements(path, tracker)?;
Ok(resolve_path(item, &resolved).is_some())
}
ConditionExpr::AttributeNotExists(path) => {
let resolved = resolve_path_elements(path, tracker)?;
Ok(resolve_path(item, &resolved).is_none())
}
ConditionExpr::AttributeType(path, type_operand) => {
let resolved = resolve_path_elements(path, tracker)?;
let val = resolve_path(item, &resolved);
let type_val = resolve_operand(type_operand, item, tracker)?;
match (val, type_val) {
(Some(v), Some(AttributeValue::S(type_name))) => Ok(v.type_name() == type_name),
_ => Ok(false),
}
}
ConditionExpr::BeginsWith(path_op, prefix_op) => {
let val = resolve_operand(path_op, item, tracker)?;
let prefix = resolve_operand(prefix_op, item, tracker)?;
match (val, prefix) {
(Some(AttributeValue::S(s)), Some(AttributeValue::S(p))) => Ok(s.starts_with(&p)),
(Some(AttributeValue::B(b)), Some(AttributeValue::B(p))) => Ok(b.starts_with(&p)),
_ => Ok(false),
}
}
ConditionExpr::Contains(path_op, search_op) => {
let val = resolve_operand(path_op, item, tracker)?;
let search = resolve_operand(search_op, item, tracker)?;
match (val, search) {
(Some(AttributeValue::S(s)), Some(AttributeValue::S(sub))) => Ok(s.contains(&sub)),
(Some(AttributeValue::B(b)), Some(AttributeValue::B(sub))) => {
Ok(sub.is_empty() || b.windows(sub.len()).any(|w| w == sub.as_slice()))
}
(Some(AttributeValue::SS(set)), Some(AttributeValue::S(elem))) => {
Ok(set.contains(&elem))
}
(Some(AttributeValue::NS(set)), Some(AttributeValue::N(elem))) => {
Ok(set.contains(&elem))
}
(Some(AttributeValue::BS(set)), Some(AttributeValue::B(elem))) => {
Ok(set.contains(&elem))
}
(Some(AttributeValue::L(list)), Some(search_val)) => Ok(list
.iter()
.any(|v| compare_values(v, &CompOp::Eq, &search_val))),
_ => Ok(false),
}
}
ConditionExpr::And(left, right) => {
if !evaluate(left, item, tracker)? {
return Ok(false); }
evaluate(right, item, tracker)
}
ConditionExpr::Or(left, right) => {
if evaluate(left, item, tracker)? {
return Ok(true); }
evaluate(right, item, tracker)
}
ConditionExpr::Not(inner) => {
let v = evaluate(inner, item, tracker)?;
Ok(!v)
}
}
}
fn parse_or(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
let mut left = parse_and(stream)?;
while matches!(stream.peek(), Some(Token::Or)) {
stream.next();
let right = parse_and(stream)?;
left = ConditionExpr::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_and(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
let mut left = parse_not(stream)?;
while matches!(stream.peek(), Some(Token::And)) {
stream.next();
let right = parse_not(stream)?;
left = ConditionExpr::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_not(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
if matches!(stream.peek(), Some(Token::Not)) {
stream.next();
let inner = parse_not(stream)?;
return Ok(ConditionExpr::Not(Box::new(inner)));
}
parse_primary(stream)
}
fn parse_primary(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
if matches!(stream.peek(), Some(Token::LParen)) {
stream.next();
let expr = parse_or(stream)?;
stream.expect(&Token::RParen)?;
return Ok(expr);
}
if let Some(Token::Identifier(name)) = stream.peek() {
let name_owned = name.clone();
let func_name = name_owned.to_lowercase();
let is_function_call = {
let saved = stream.pos();
stream.next(); let is_lparen = matches!(stream.peek(), Some(Token::LParen));
stream.set_pos(saved); is_lparen
};
if is_function_call {
match func_name.as_str() {
"attribute_exists" => {
stream.next();
stream.expect(&Token::LParen)?;
let path = parse_raw_path(stream)?;
stream.expect(&Token::RParen)?;
return Ok(ConditionExpr::AttributeExists(path));
}
"attribute_not_exists" => {
stream.next();
stream.expect(&Token::LParen)?;
let path = parse_raw_path(stream)?;
stream.expect(&Token::RParen)?;
return Ok(ConditionExpr::AttributeNotExists(path));
}
"attribute_type" => {
stream.next();
stream.expect(&Token::LParen)?;
let path = parse_raw_path(stream)?;
stream.expect(&Token::Comma)?;
let type_val = parse_operand(stream)?;
stream.expect(&Token::RParen)?;
return Ok(ConditionExpr::AttributeType(path, type_val));
}
"begins_with" => {
stream.next();
stream.expect(&Token::LParen)?;
let path_op = parse_operand(stream)?;
stream.expect(&Token::Comma)?;
let prefix_op = parse_operand(stream)?;
stream.expect(&Token::RParen)?;
return Ok(ConditionExpr::BeginsWith(path_op, prefix_op));
}
"contains" => {
stream.next();
stream.expect(&Token::LParen)?;
let path_op = parse_operand(stream)?;
stream.expect(&Token::Comma)?;
let search_op = parse_operand(stream)?;
stream.expect(&Token::RParen)?;
return Ok(ConditionExpr::Contains(path_op, search_op));
}
"size" => {
}
_ => {
return Err(format!("Invalid function name; function: {}", name_owned));
}
}
} else {
}
}
let left = parse_operand(stream)?;
match stream.peek() {
Some(Token::Eq) => {
stream.next();
let right = parse_operand(stream)?;
Ok(ConditionExpr::Comparison {
left,
op: CompOp::Eq,
right,
})
}
Some(Token::Ne) => {
stream.next();
let right = parse_operand(stream)?;
Ok(ConditionExpr::Comparison {
left,
op: CompOp::Ne,
right,
})
}
Some(Token::Lt) => {
stream.next();
let right = parse_operand(stream)?;
Ok(ConditionExpr::Comparison {
left,
op: CompOp::Lt,
right,
})
}
Some(Token::Le) => {
stream.next();
let right = parse_operand(stream)?;
Ok(ConditionExpr::Comparison {
left,
op: CompOp::Le,
right,
})
}
Some(Token::Gt) => {
stream.next();
let right = parse_operand(stream)?;
Ok(ConditionExpr::Comparison {
left,
op: CompOp::Gt,
right,
})
}
Some(Token::Ge) => {
stream.next();
let right = parse_operand(stream)?;
Ok(ConditionExpr::Comparison {
left,
op: CompOp::Ge,
right,
})
}
Some(Token::Between) => {
stream.next();
let lo = parse_operand(stream)?;
stream.expect(&Token::And)?;
let hi = parse_operand(stream)?;
Ok(ConditionExpr::Between {
operand: left,
lo,
hi,
})
}
Some(Token::In) => {
stream.next();
stream.expect(&Token::LParen)?;
let mut values = vec![parse_operand(stream)?];
while matches!(stream.peek(), Some(Token::Comma)) {
stream.next();
values.push(parse_operand(stream)?);
}
stream.expect(&Token::RParen)?;
Ok(ConditionExpr::In {
operand: left,
values,
})
}
_ => Err("Expected comparison operator, BETWEEN, or IN".to_string()),
}
}
fn parse_operand(stream: &mut TokenStream) -> Result<Operand, String> {
if let Some(Token::Identifier(name)) = stream.peek() {
if name.to_lowercase() == "size" {
stream.next();
stream.expect(&Token::LParen)?;
let path = parse_raw_path(stream)?;
stream.expect(&Token::RParen)?;
return Ok(Operand::Size(path));
}
}
match stream.peek() {
Some(Token::ValueRef(_)) => {
if let Some(Token::ValueRef(name)) = stream.next().cloned() {
Ok(Operand::ValueRef(name))
} else {
unreachable!()
}
}
Some(Token::Identifier(_)) | Some(Token::NameRef(_)) => {
let path = parse_raw_path(stream)?;
Ok(Operand::Path(path))
}
Some(t) => Err(format!("Expected operand, got {t}")),
None => Err("Expected operand, got end of expression".to_string()),
}
}
pub fn parse_raw_path(stream: &mut TokenStream) -> Result<Vec<PathElement>, String> {
let first = match stream.next() {
Some(Token::Identifier(name)) => {
if super::reserved::is_reserved_keyword(name) {
return Err(format!(
"Attribute name is a reserved keyword; reserved keyword: {name}"
));
}
PathElement::Attribute(name.clone())
}
Some(Token::NameRef(name)) => PathElement::Attribute(name.clone()),
Some(t) => return Err(format!("Expected attribute name, got {t}")),
None => return Err("Expected attribute name, got end of expression".to_string()),
};
let mut path = vec![first];
loop {
match stream.peek() {
Some(Token::Dot) => {
stream.next();
match stream.next() {
Some(Token::Identifier(name)) => {
if super::reserved::is_reserved_keyword(name) {
return Err(format!(
"Attribute name is a reserved keyword; reserved keyword: {name}"
));
}
path.push(PathElement::Attribute(name.clone()));
}
Some(Token::NameRef(name)) => {
path.push(PathElement::Attribute(name.clone()));
}
Some(t) => return Err(format!("Expected attribute name after '.', got {t}")),
None => return Err("Expected attribute name after '.'".to_string()),
}
}
Some(Token::LBracket) => {
stream.next();
match stream.next() {
Some(Token::Number(n)) => {
let idx: usize = n.parse().map_err(|_| format!("Invalid index: {n}"))?;
path.push(PathElement::Index(idx));
}
Some(t) => return Err(format!("Expected number in brackets, got {t}")),
None => return Err("Expected number in brackets".to_string()),
}
stream.expect(&Token::RBracket)?;
}
_ => break,
}
}
Ok(path)
}
fn resolve_operand(
operand: &Operand,
item: &HashMap<String, AttributeValue>,
tracker: &TrackedExpressionAttributes,
) -> Result<Option<AttributeValue>, String> {
match operand {
Operand::Path(path) => {
let resolved = resolve_path_elements(path, tracker)?;
Ok(resolve_path(item, &resolved))
}
Operand::ValueRef(name) => {
let val = tracker.resolve_value(name)?;
Ok(Some(val.clone()))
}
Operand::Size(path) => {
let resolved = resolve_path_elements(path, tracker)?;
match resolve_path(item, &resolved) {
Some(val) => {
let size = match &val {
AttributeValue::S(s) => s.encode_utf16().count(),
AttributeValue::B(b) => b.len(),
AttributeValue::SS(set) => set.len(),
AttributeValue::NS(set) => set.len(),
AttributeValue::BS(set) => set.len(),
AttributeValue::L(list) => list.len(),
AttributeValue::M(map) => map.len(),
_ => return Ok(None),
};
Ok(Some(AttributeValue::N(size.to_string())))
}
None => Ok(None),
}
}
}
}
fn compare_values(left: &AttributeValue, op: &CompOp, right: &AttributeValue) -> bool {
match (left, right) {
(AttributeValue::S(a), AttributeValue::S(b)) => compare_ord(a, b, op),
(AttributeValue::N(a), AttributeValue::N(b)) => {
if can_use_f64(a) && can_use_f64(b) {
if let (Ok(fa), Ok(fb)) = (a.parse::<f64>(), b.parse::<f64>()) {
if fa.is_finite() && fb.is_finite() {
return compare_ord(&fa, &fb, op);
}
}
}
use bigdecimal::BigDecimal;
use std::str::FromStr;
match (BigDecimal::from_str(a), BigDecimal::from_str(b)) {
(Ok(da), Ok(db)) => compare_ord(&da, &db, op),
_ => false,
}
}
(AttributeValue::B(a), AttributeValue::B(b)) => compare_ord(a, b, op),
(AttributeValue::BOOL(a), AttributeValue::BOOL(b)) => match op {
CompOp::Eq => a == b,
CompOp::Ne => a != b,
_ => false,
},
(AttributeValue::NULL(a), AttributeValue::NULL(b)) => match op {
CompOp::Eq => a == b,
CompOp::Ne => a != b,
_ => false,
},
(AttributeValue::SS(a), AttributeValue::SS(b)) => {
let mut sa = a.clone();
let mut sb = b.clone();
sa.sort();
sb.sort();
match op {
CompOp::Eq => sa == sb,
CompOp::Ne => sa != sb,
_ => false,
}
}
(AttributeValue::NS(a), AttributeValue::NS(b)) => {
if a.len() != b.len() {
return matches!(op, CompOp::Ne);
}
let mut fa: Vec<f64> = match a.iter().map(|n| n.parse::<f64>()).collect() {
Ok(v) => v,
Err(_) => return false,
};
let mut fb: Vec<f64> = match b.iter().map(|n| n.parse::<f64>()).collect() {
Ok(v) => v,
Err(_) => return false,
};
fa.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
fb.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
match op {
CompOp::Eq => fa == fb,
CompOp::Ne => fa != fb,
_ => false,
}
}
(AttributeValue::BS(a), AttributeValue::BS(b)) => {
let mut sa = a.clone();
let mut sb = b.clone();
sa.sort();
sb.sort();
match op {
CompOp::Eq => sa == sb,
CompOp::Ne => sa != sb,
_ => false,
}
}
_ => matches!(op, CompOp::Ne),
}
}
fn compare_ord<T: PartialOrd>(a: &T, b: &T, op: &CompOp) -> bool {
match op {
CompOp::Eq => a == b,
CompOp::Ne => a != b,
CompOp::Lt => a < b,
CompOp::Le => a <= b,
CompOp::Gt => a > b,
CompOp::Ge => a >= b,
}
}
pub fn track_references(
expr: &ConditionExpr,
tracker: &TrackedExpressionAttributes,
) -> Result<(), String> {
match expr {
ConditionExpr::Comparison { left, op: _, right } => {
track_operand_refs(left, tracker)?;
track_operand_refs(right, tracker)
}
ConditionExpr::Between { operand, lo, hi } => {
track_operand_refs(operand, tracker)?;
track_operand_refs(lo, tracker)?;
track_operand_refs(hi, tracker)
}
ConditionExpr::In { operand, values } => {
track_operand_refs(operand, tracker)?;
for v in values {
track_operand_refs(v, tracker)?;
}
Ok(())
}
ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
track_cond_path_refs(path, tracker)
}
ConditionExpr::AttributeType(path, type_op) => {
track_cond_path_refs(path, tracker)?;
track_operand_refs(type_op, tracker)
}
ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
track_operand_refs(a, tracker)?;
track_operand_refs(b, tracker)
}
ConditionExpr::And(left, right) | ConditionExpr::Or(left, right) => {
track_references(left, tracker)?;
track_references(right, tracker)
}
ConditionExpr::Not(inner) => track_references(inner, tracker),
}
}
fn track_operand_refs(
operand: &Operand,
tracker: &TrackedExpressionAttributes,
) -> Result<(), String> {
match operand {
Operand::Path(path) => track_cond_path_refs(path, tracker),
Operand::ValueRef(name) => {
tracker.resolve_value(name)?;
Ok(())
}
Operand::Size(path) => track_cond_path_refs(path, tracker),
}
}
fn track_cond_path_refs(
path: &[PathElement],
tracker: &TrackedExpressionAttributes,
) -> Result<(), String> {
for elem in path {
if let PathElement::Attribute(name) = elem {
if name.starts_with('#') {
tracker.resolve_name(name)?;
}
}
}
Ok(())
}
pub fn validate_operand_semantics(
expr: &ConditionExpr,
names: &Option<HashMap<String, String>>,
values: &Option<HashMap<String, AttributeValue>>,
) -> Result<(), String> {
match expr {
ConditionExpr::Contains(first, rest) => {
if first == rest {
return Err(format!(
"The first operand must be distinct from the remaining operands for this operator or function; operator: contains, first operand: [{}]",
render_operand_name(first, names)
));
}
Ok(())
}
ConditionExpr::BeginsWith(_, prefix) => {
if let Operand::ValueRef(vname) = prefix {
if let Some(v) = values.as_ref().and_then(|m| m.get(vname.as_str())) {
if !matches!(v, AttributeValue::S(_) | AttributeValue::B(_)) {
return Err(format!(
"Incorrect operand type for operator or function; operator or function: begins_with, operand type: {}",
v.type_name()
));
}
}
}
Ok(())
}
ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
validate_operand_semantics(a, names, values)?;
validate_operand_semantics(b, names, values)
}
ConditionExpr::Not(inner) => validate_operand_semantics(inner, names, values),
_ => Ok(()),
}
}
fn render_operand_name(op: &Operand, names: &Option<HashMap<String, String>>) -> String {
let elems = match op {
Operand::Path(elems) | Operand::Size(elems) => elems,
Operand::ValueRef(name) => return name.clone(),
};
elems
.iter()
.map(|e| match e {
PathElement::Attribute(a) if a.starts_with('#') => names
.as_ref()
.and_then(|m| m.get(a))
.cloned()
.unwrap_or_else(|| a.clone()),
PathElement::Attribute(a) => a.clone(),
PathElement::Index(i) => format!("[{i}]"),
})
.collect::<Vec<_>>()
.join(".")
}
pub fn validate_static(
expr: &ConditionExpr,
values: &Option<HashMap<String, AttributeValue>>,
) -> Result<(), String> {
match expr {
ConditionExpr::Between { operand: _, lo, hi } => {
if let (Operand::ValueRef(lo_name), Operand::ValueRef(hi_name)) = (lo, hi) {
if let Some(vals) = values {
let lo_val = vals.get(lo_name.as_str());
let hi_val = vals.get(hi_name.as_str());
if let (Some(lo_v), Some(hi_v)) = (lo_val, hi_val) {
if std::mem::discriminant(lo_v) != std::mem::discriminant(hi_v) {
return Err(format!(
"Invalid ConditionExpression: The BETWEEN operator requires same data type for lower and upper bounds; \
lower bound operand: AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
format_av_for_error(lo_v),
format_av_for_error(hi_v),
));
}
if compare_values(lo_v, &CompOp::Gt, hi_v) {
return Err(format!(
"Invalid ConditionExpression: The BETWEEN operator requires upper bound to be greater than or equal to lower bound; \
lower bound operand: AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
format_av_for_error(lo_v),
format_av_for_error(hi_v),
));
}
}
}
}
Ok(())
}
ConditionExpr::And(left, right) | ConditionExpr::Or(left, right) => {
validate_static(left, values)?;
validate_static(right, values)
}
ConditionExpr::Not(inner) => validate_static(inner, values),
_ => Ok(()),
}
}
fn format_av_for_error(av: &AttributeValue) -> String {
match av {
AttributeValue::S(s) => format!("S:{s}"),
AttributeValue::N(n) => format!("N:{n}"),
AttributeValue::B(b) => {
use base64::Engine;
format!("B:{}", base64::engine::general_purpose::STANDARD.encode(b))
}
AttributeValue::BOOL(b) => format!("BOOL:{b}"),
AttributeValue::NULL(_) => "NULL:true".to_string(),
AttributeValue::SS(set) => format!("SS:{set:?}"),
AttributeValue::NS(set) => format!("NS:{set:?}"),
AttributeValue::BS(_) => "BS:[...]".to_string(),
AttributeValue::L(_) => "L:[...]".to_string(),
AttributeValue::M(_) => "M:{...}".to_string(),
}
}
pub fn check_non_scalar_key_access(
expr: &ConditionExpr,
attr_names: &Option<HashMap<String, String>>,
key_attrs: &[String],
index_key_attrs: &[String],
) -> Option<(String, bool)> {
let mut result = None;
check_non_scalar_key_access_inner(expr, attr_names, key_attrs, index_key_attrs, &mut result);
result
}
fn check_non_scalar_key_access_inner(
expr: &ConditionExpr,
attr_names: &Option<HashMap<String, String>>,
key_attrs: &[String],
index_key_attrs: &[String],
result: &mut Option<(String, bool)>,
) {
if result.is_some() {
return;
}
match expr {
ConditionExpr::Comparison { left, right, .. } => {
check_operand_non_scalar(left, attr_names, key_attrs, index_key_attrs, result);
check_operand_non_scalar(right, attr_names, key_attrs, index_key_attrs, result);
}
ConditionExpr::Between { operand, lo, hi } => {
check_operand_non_scalar(operand, attr_names, key_attrs, index_key_attrs, result);
check_operand_non_scalar(lo, attr_names, key_attrs, index_key_attrs, result);
check_operand_non_scalar(hi, attr_names, key_attrs, index_key_attrs, result);
}
ConditionExpr::In { operand, values } => {
check_operand_non_scalar(operand, attr_names, key_attrs, index_key_attrs, result);
for v in values {
check_operand_non_scalar(v, attr_names, key_attrs, index_key_attrs, result);
}
}
ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
}
ConditionExpr::AttributeType(path, _) => {
check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
}
ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
check_operand_non_scalar(a, attr_names, key_attrs, index_key_attrs, result);
check_operand_non_scalar(b, attr_names, key_attrs, index_key_attrs, result);
}
ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
check_non_scalar_key_access_inner(a, attr_names, key_attrs, index_key_attrs, result);
check_non_scalar_key_access_inner(b, attr_names, key_attrs, index_key_attrs, result);
}
ConditionExpr::Not(inner) => {
check_non_scalar_key_access_inner(
inner,
attr_names,
key_attrs,
index_key_attrs,
result,
);
}
}
}
fn check_operand_non_scalar(
operand: &Operand,
attr_names: &Option<HashMap<String, String>>,
key_attrs: &[String],
index_key_attrs: &[String],
result: &mut Option<(String, bool)>,
) {
if result.is_some() {
return;
}
match operand {
Operand::Path(path) | Operand::Size(path) => {
check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
}
Operand::ValueRef(_) => {}
}
}
fn check_path_non_scalar(
path: &[PathElement],
attr_names: &Option<HashMap<String, String>>,
key_attrs: &[String],
index_key_attrs: &[String],
result: &mut Option<(String, bool)>,
) {
if result.is_some() || path.len() <= 1 {
return; }
if let Some(name) = resolve_top_level_path(path, attr_names) {
if key_attrs.contains(&name) {
*result = Some((name, false));
} else if index_key_attrs.contains(&name) {
*result = Some((name, true));
}
}
}
pub fn extract_top_level_attributes(
expr: &ConditionExpr,
attr_names: &Option<HashMap<String, String>>,
) -> Vec<String> {
let mut attrs = Vec::new();
collect_top_level_attrs(expr, attr_names, &mut attrs);
attrs.sort();
attrs.dedup();
attrs
}
fn collect_top_level_attrs(
expr: &ConditionExpr,
attr_names: &Option<HashMap<String, String>>,
out: &mut Vec<String>,
) {
match expr {
ConditionExpr::Comparison { left, right, .. } => {
collect_operand_top_attr(left, attr_names, out);
collect_operand_top_attr(right, attr_names, out);
}
ConditionExpr::Between { operand, lo, hi } => {
collect_operand_top_attr(operand, attr_names, out);
collect_operand_top_attr(lo, attr_names, out);
collect_operand_top_attr(hi, attr_names, out);
}
ConditionExpr::In { operand, values } => {
collect_operand_top_attr(operand, attr_names, out);
for v in values {
collect_operand_top_attr(v, attr_names, out);
}
}
ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
if let Some(name) = resolve_top_level_path(path, attr_names) {
out.push(name);
}
}
ConditionExpr::AttributeType(path, _) => {
if let Some(name) = resolve_top_level_path(path, attr_names) {
out.push(name);
}
}
ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
collect_operand_top_attr(a, attr_names, out);
collect_operand_top_attr(b, attr_names, out);
}
ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
collect_top_level_attrs(a, attr_names, out);
collect_top_level_attrs(b, attr_names, out);
}
ConditionExpr::Not(inner) => {
collect_top_level_attrs(inner, attr_names, out);
}
}
}
fn collect_operand_top_attr(
operand: &Operand,
attr_names: &Option<HashMap<String, String>>,
out: &mut Vec<String>,
) {
match operand {
Operand::Path(path) => {
if let Some(name) = resolve_top_level_path(path, attr_names) {
out.push(name);
}
}
Operand::Size(path) => {
if let Some(name) = resolve_top_level_path(path, attr_names) {
out.push(name);
}
}
Operand::ValueRef(_) => {}
}
}
fn resolve_top_level_path(
path: &[PathElement],
attr_names: &Option<HashMap<String, String>>,
) -> Option<String> {
match path.first() {
Some(PathElement::Attribute(name)) => {
if name.starts_with('#') {
attr_names
.as_ref()
.and_then(|m| m.get(name.as_str()))
.cloned()
} else {
Some(name.clone())
}
}
_ => None,
}
}
pub fn validate_name_refs(
expr: &ConditionExpr,
attr_names: &Option<HashMap<String, String>>,
) -> Result<(), String> {
let mut undefined = Vec::new();
collect_undefined_name_refs(expr, attr_names, &mut undefined);
if let Some(name) = undefined.first() {
Err(format!(
"An expression attribute name used in the document path is not defined; attribute name: {}",
name
))
} else {
Ok(())
}
}
fn collect_undefined_name_refs(
expr: &ConditionExpr,
attr_names: &Option<HashMap<String, String>>,
out: &mut Vec<String>,
) {
match expr {
ConditionExpr::Comparison { left, right, .. } => {
collect_operand_undefined_refs(left, attr_names, out);
collect_operand_undefined_refs(right, attr_names, out);
}
ConditionExpr::Between { operand, lo, hi } => {
collect_operand_undefined_refs(operand, attr_names, out);
collect_operand_undefined_refs(lo, attr_names, out);
collect_operand_undefined_refs(hi, attr_names, out);
}
ConditionExpr::In { operand, values } => {
collect_operand_undefined_refs(operand, attr_names, out);
for v in values {
collect_operand_undefined_refs(v, attr_names, out);
}
}
ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
collect_path_undefined_refs(path, attr_names, out);
}
ConditionExpr::AttributeType(path, operand) => {
collect_path_undefined_refs(path, attr_names, out);
collect_operand_undefined_refs(operand, attr_names, out);
}
ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
collect_operand_undefined_refs(a, attr_names, out);
collect_operand_undefined_refs(b, attr_names, out);
}
ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
collect_undefined_name_refs(a, attr_names, out);
collect_undefined_name_refs(b, attr_names, out);
}
ConditionExpr::Not(inner) => {
collect_undefined_name_refs(inner, attr_names, out);
}
}
}
fn collect_operand_undefined_refs(
operand: &Operand,
attr_names: &Option<HashMap<String, String>>,
out: &mut Vec<String>,
) {
match operand {
Operand::Path(path) | Operand::Size(path) => {
collect_path_undefined_refs(path, attr_names, out);
}
Operand::ValueRef(_) => {}
}
}
fn collect_path_undefined_refs(
path: &[PathElement],
attr_names: &Option<HashMap<String, String>>,
out: &mut Vec<String>,
) {
for elem in path {
if let PathElement::Attribute(name) = elem {
if name.starts_with('#') {
let defined = attr_names
.as_ref()
.is_some_and(|m| m.contains_key(name.as_str()));
if !defined && !out.contains(name) {
out.push(name.clone());
}
}
}
}
}
fn can_use_f64(s: &str) -> bool {
if s.contains('E') || s.contains('e') {
return false;
}
let digit_count = s.bytes().filter(|b| b.is_ascii_digit()).count();
digit_count <= 15
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expressions::evaluate_without_tracking;
fn make_item(pairs: &[(&str, AttributeValue)]) -> HashMap<String, AttributeValue> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
fn vals(pairs: &[(&str, AttributeValue)]) -> Option<HashMap<String, AttributeValue>> {
Some(make_item(pairs))
}
fn names(pairs: &[(&str, &str)]) -> Option<HashMap<String, String>> {
Some(
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
}
#[test]
fn test_simple_equality() {
let expr = parse("pk = :val").unwrap();
let item = make_item(&[("pk", AttributeValue::S("hello".into()))]);
let av = vals(&[(":val", AttributeValue::S("hello".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_inequality() {
let expr = parse("pk <> :val").unwrap();
let item = make_item(&[("pk", AttributeValue::S("hello".into()))]);
let av = vals(&[(":val", AttributeValue::S("world".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_numeric_comparison() {
let expr = parse("price > :min").unwrap();
let item = make_item(&[("price", AttributeValue::N("42".into()))]);
let av = vals(&[(":min", AttributeValue::N("10".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_between() {
let expr = parse("age BETWEEN :lo AND :hi").unwrap();
let item = make_item(&[("age", AttributeValue::N("25".into()))]);
let av = vals(&[
(":lo", AttributeValue::N("18".into())),
(":hi", AttributeValue::N("65".into())),
]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_in_operator() {
let expr = parse("state_val IN (:s1, :s2, :s3)").unwrap();
let item = make_item(&[("state_val", AttributeValue::S("active".into()))]);
let av = vals(&[
(":s1", AttributeValue::S("active".into())),
(":s2", AttributeValue::S("pending".into())),
(":s3", AttributeValue::S("closed".into())),
]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_attribute_exists() {
let expr = parse("attribute_exists(email)").unwrap();
let item = make_item(&[("email", AttributeValue::S("a@b.com".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &None).unwrap());
let empty_item: HashMap<String, AttributeValue> = HashMap::new();
assert!(!evaluate_without_tracking(&expr, &empty_item, &None, &None).unwrap());
}
#[test]
fn test_attribute_not_exists() {
let expr = parse("attribute_not_exists(email)").unwrap();
let item: HashMap<String, AttributeValue> = HashMap::new();
assert!(evaluate_without_tracking(&expr, &item, &None, &None).unwrap());
}
#[test]
fn test_begins_with() {
let expr = parse("begins_with(sk, :prefix)").unwrap();
let item = make_item(&[("sk", AttributeValue::S("user#123".into()))]);
let av = vals(&[(":prefix", AttributeValue::S("user#".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_contains_string() {
let expr = parse("contains(description, :sub)").unwrap();
let item = make_item(&[("description", AttributeValue::S("hello world".into()))]);
let av = vals(&[(":sub", AttributeValue::S("world".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_contains_string_set() {
let expr = parse("contains(tags, :tag)").unwrap();
let item = make_item(&[(
"tags",
AttributeValue::SS(vec!["rust".into(), "dynamo".into()]),
)]);
let av = vals(&[(":tag", AttributeValue::S("rust".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_size_function() {
let expr = parse("size(label) > :len").unwrap();
let item = make_item(&[("label", AttributeValue::S("Alice".into()))]);
let av = vals(&[(":len", AttributeValue::N("3".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_and_operator() {
let expr = parse("price > :min AND price < :max").unwrap();
let item = make_item(&[("price", AttributeValue::N("50".into()))]);
let av = vals(&[
(":min", AttributeValue::N("10".into())),
(":max", AttributeValue::N("100".into())),
]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_or_operator() {
let expr = parse("state_val = :s1 OR state_val = :s2").unwrap();
let item = make_item(&[("state_val", AttributeValue::S("pending".into()))]);
let av = vals(&[
(":s1", AttributeValue::S("active".into())),
(":s2", AttributeValue::S("pending".into())),
]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_not_operator() {
let expr = parse("NOT state_val = :val").unwrap();
let item = make_item(&[("state_val", AttributeValue::S("active".into()))]);
let av = vals(&[(":val", AttributeValue::S("closed".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_expression_attribute_names() {
let expr = parse("#s = :val").unwrap();
let item = make_item(&[("status", AttributeValue::S("active".into()))]);
let an = names(&[("#s", "status")]);
let av = vals(&[(":val", AttributeValue::S("active".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &an, &av).unwrap());
}
#[test]
fn test_nested_path() {
let expr = parse("profile.label = :val").unwrap();
let mut nested = HashMap::new();
nested.insert("label".to_string(), AttributeValue::S("Alice".into()));
let item = make_item(&[("profile", AttributeValue::M(nested))]);
let av = vals(&[(":val", AttributeValue::S("Alice".into()))]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_parenthesized() {
let expr = parse("(a = :x OR b = :y) AND c = :z").unwrap();
let item = make_item(&[
("a", AttributeValue::S("1".into())),
("b", AttributeValue::S("2".into())),
("c", AttributeValue::S("3".into())),
]);
let av = vals(&[
(":x", AttributeValue::S("wrong".into())),
(":y", AttributeValue::S("2".into())),
(":z", AttributeValue::S("3".into())),
]);
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_missing_attribute_is_false() {
let expr = parse("nonexistent = :val").unwrap();
let item: HashMap<String, AttributeValue> = HashMap::new();
let av = vals(&[(":val", AttributeValue::S("x".into()))]);
assert!(!evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_missing_attribute_ne_is_true() {
let item: HashMap<String, AttributeValue> = HashMap::new();
let av = vals(&[(":val", AttributeValue::S("working".into()))]);
let expr = parse("nonexistent <> :val").unwrap();
assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
}
#[test]
fn test_missing_attribute_comparisons() {
let item: HashMap<String, AttributeValue> = HashMap::new();
let av = vals(&[(":val", AttributeValue::S("x".into()))]);
for (op, expected) in [
("=", false),
("<>", true),
("<", false),
("<=", false),
(">", false),
(">=", false),
] {
let expr = parse(&format!("nonexistent {} :val", op)).unwrap();
assert_eq!(
evaluate_without_tracking(&expr, &item, &None, &av).unwrap(),
expected,
"operator {} on missing attribute should be {}",
op,
expected
);
}
}
}