use aws_sdk_dynamodb::types::AttributeValue;
use serde_json::Value as Json;
use crate::attribute::serialize_json;
use crate::errors::GraphDDBError;
use crate::value::indexmap_shim::IndexMap;
const OPERATOR_KEYS: &[&str] = &[
"eq",
"ne",
"gt",
"ge",
"lt",
"le",
"between",
"in",
"beginsWith",
"contains",
"notContains",
"attributeExists",
"attributeType",
"size",
];
const LOGICAL_KEYS: &[&str] = &["and", "or", "not"];
#[derive(Debug, Clone)]
pub struct CompiledFilter {
pub expression: String,
pub names: IndexMap<String>,
pub values: IndexMap<AttributeValue>,
}
struct Ctx {
names: IndexMap<String>,
values: IndexMap<AttributeValue>,
name_n: usize,
value_n: usize,
}
impl Ctx {
fn new() -> Self {
Self {
names: IndexMap::new(),
values: IndexMap::new(),
name_n: 0,
value_n: 0,
}
}
fn name_alias(&mut self, column: &str) -> String {
for (alias, col) in self.names.iter() {
if col == column {
return alias.clone();
}
}
let alias = format!("#f{}", self.name_n);
self.name_n += 1;
self.names.insert(alias.clone(), column.to_string());
alias
}
fn value_alias(&mut self, raw: &Json) -> Result<String, GraphDDBError> {
let alias = format!(":vf{}", self.value_n);
self.value_n += 1;
self.values.insert(alias.clone(), serialize_json(raw)?);
Ok(alias)
}
}
fn is_operator_object(value: &Json) -> bool {
match value.as_object() {
Some(obj) if !obj.is_empty() => obj.keys().all(|k| OPERATOR_KEYS.contains(&k.as_str())),
_ => false,
}
}
fn is_already_wrapped(expr: &str) -> bool {
let bytes = expr.as_bytes();
if !(expr.starts_with('(') && expr.ends_with(')')) {
return false;
}
let mut depth = 0i32;
for (i, &ch) in bytes.iter().enumerate() {
if ch == b'(' {
depth += 1;
} else if ch == b')' {
depth -= 1;
if depth == 0 && i < bytes.len() - 1 {
return false;
}
}
}
depth == 0
}
fn wrap(expr: &str) -> String {
if is_already_wrapped(expr) {
return expr.to_string();
}
if expr.contains(" AND ") || expr.contains(" OR ") {
format!("({expr})")
} else {
expr.to_string()
}
}
fn join_and(clauses: &[String]) -> String {
if clauses.len() == 1 {
return clauses[0].clone();
}
clauses
.iter()
.map(|c| wrap(c))
.collect::<Vec<_>>()
.join(" AND ")
}
fn compile_field(ctx: &mut Ctx, field: &str, condition: &Json) -> Result<String, GraphDDBError> {
let n = ctx.name_alias(field);
if !is_operator_object(condition) {
let v = ctx.value_alias(condition)?;
return Ok(format!("{n} = {v}"));
}
let obj = condition.as_object().unwrap();
let mut clauses: Vec<String> = Vec::new();
for (op, value) in obj {
let clause = match op.as_str() {
"eq" => format!("{n} = {}", ctx.value_alias(value)?),
"ne" => format!("{n} <> {}", ctx.value_alias(value)?),
"gt" => format!("{n} > {}", ctx.value_alias(value)?),
"ge" => format!("{n} >= {}", ctx.value_alias(value)?),
"lt" => format!("{n} < {}", ctx.value_alias(value)?),
"le" => format!("{n} <= {}", ctx.value_alias(value)?),
"between" => {
let arr = value.as_array().filter(|a| a.len() == 2).ok_or_else(|| {
GraphDDBError::new(format!(
"between operator on field '{field}' expects a [lo, hi] array of length 2"
))
})?;
let lo = ctx.value_alias(&arr[0])?;
let hi = ctx.value_alias(&arr[1])?;
format!("{n} BETWEEN {lo} AND {hi}")
}
"in" => {
let arr = value
.as_array()
.ok_or_else(|| GraphDDBError::new("in operator expects an array"))?;
let mut aliases = Vec::with_capacity(arr.len());
for v in arr {
aliases.push(ctx.value_alias(v)?);
}
format!("{n} IN ({})", aliases.join(", "))
}
"beginsWith" => format!("begins_with({n}, {})", ctx.value_alias(value)?),
"contains" => format!("contains({n}, {})", ctx.value_alias(value)?),
"notContains" => format!("NOT contains({n}, {})", ctx.value_alias(value)?),
"attributeExists" => {
if value == &Json::Bool(false) {
format!("attribute_not_exists({n})")
} else {
format!("attribute_exists({n})")
}
}
"attributeType" => format!("attribute_type({n}, {})", ctx.value_alias(value)?),
"size" => format!("size({n}) = {}", ctx.value_alias(value)?),
other => {
return Err(GraphDDBError::new(format!(
"Unknown filter operator '{other}' on field '{field}'"
)))
}
};
clauses.push(clause);
}
Ok(join_and(&clauses))
}
fn compile_node(ctx: &mut Ctx, node: &Json) -> Result<String, GraphDDBError> {
let obj = match node.as_object() {
Some(o) => o,
None => return Ok(String::new()),
};
let mut clauses: Vec<String> = Vec::new();
for (key, value) in obj {
if value.is_null() {
continue;
}
if LOGICAL_KEYS.contains(&key.as_str()) {
if key == "and" || key == "or" {
let arr = value.as_array().cloned().unwrap_or_default();
let mut parts: Vec<String> = Vec::new();
for s in &arr {
let p = compile_node(ctx, s)?;
if !p.is_empty() {
parts.push(p);
}
}
if parts.is_empty() {
continue;
}
if parts.len() == 1 {
clauses.push(parts.remove(0));
} else {
let sep = if key == "and" { " AND " } else { " OR " };
let joined = parts.iter().map(|p| wrap(p)).collect::<Vec<_>>().join(sep);
clauses.push(format!("({joined})"));
}
} else {
let inner = compile_node(ctx, value)?;
if !inner.is_empty() {
clauses.push(format!("NOT {}", wrap(&inner)));
}
}
continue;
}
let clause = compile_field(ctx, key, value)?;
if !clause.is_empty() {
clauses.push(clause);
}
}
Ok(join_and(&clauses))
}
pub fn compile_filter(declarative: &Json) -> Result<Option<CompiledFilter>, GraphDDBError> {
let obj = match declarative.as_object() {
Some(o) if !o.is_empty() => o,
_ => return Ok(None),
};
let _ = obj;
let mut ctx = Ctx::new();
let expr = compile_node(&mut ctx, declarative)?;
if expr.is_empty() {
return Ok(None);
}
Ok(Some(CompiledFilter {
expression: expr,
names: ctx.names,
values: ctx.values,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn simple_eq() {
let c = compile_filter(&json!({"role": "admin"})).unwrap().unwrap();
assert_eq!(c.expression, "#f0 = :vf0");
assert_eq!(c.names.get("#f0").unwrap(), "role");
}
#[test]
fn and_of_ops_wraps_correctly() {
let f = json!({"and": [{"role": {"in": ["a", "b"]}}, {"role": {"beginsWith": "x"}}]});
let c = compile_filter(&f).unwrap().unwrap();
assert_eq!(
c.expression,
"(#f0 IN (:vf0, :vf1) AND begins_with(#f0, :vf2))"
);
}
#[test]
fn not_and_between_and_size() {
let c = compile_filter(&json!({"role": {"size": 3}}))
.unwrap()
.unwrap();
assert_eq!(c.expression, "size(#f0) = :vf0");
let c2 = compile_filter(&json!({"not": {"role": "admin"}}))
.unwrap()
.unwrap();
assert_eq!(c2.expression, "NOT #f0 = :vf0");
let c3 = compile_filter(&json!({"n": {"between": [1, 5]}}))
.unwrap()
.unwrap();
assert_eq!(c3.expression, "#f0 BETWEEN :vf0 AND :vf1");
}
#[test]
fn empty_is_none() {
assert!(compile_filter(&json!({})).unwrap().is_none());
}
}