use core::fmt;
use std::borrow::Cow;
use super::{
super::IntoAttributeValue, ApplyCondition, ApplyExpressionAttributes, ApplyFilter,
ApplyKeyCondition, AttrNames, AttrValues, AttributeValue, ConditionableBuilder, Expression,
FilterableBuilder, KeyConditionableBuilder, fmt_attr_maps, resolve_expression,
utils::resolve_attr_path,
};
#[derive(Debug, Clone, Copy)]
pub enum Comparison {
Eq,
Neq,
Lt,
Le,
Gt,
Ge,
}
impl fmt::Display for Comparison {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Comparison::Eq => "=",
Comparison::Neq => "<>",
Comparison::Lt => "<",
Comparison::Le => "<=",
Comparison::Gt => ">",
Comparison::Ge => ">=",
})
}
}
#[derive(Debug, Clone)]
enum ConditionInner<'a> {
And(Vec<Condition<'a>>),
Or(Vec<Condition<'a>>),
Not(Box<Condition<'a>>),
Compare {
attr: Cow<'a, str>,
cmp: Comparison,
value: AttributeValue,
},
Between {
attr: Cow<'a, str>,
low: AttributeValue,
high: AttributeValue,
},
In {
attr: Cow<'a, str>,
values: Vec<AttributeValue>,
},
Exists(Cow<'a, str>),
NotExists(Cow<'a, str>),
BeginsWith {
attr: Cow<'a, str>,
prefix: AttributeValue,
},
Contains {
attr: Cow<'a, str>,
value: AttributeValue,
},
SizeCompare {
attr: Cow<'a, str>,
cmp: Comparison,
value: AttributeValue,
},
}
#[derive(Debug, Clone)]
#[must_use = "condition does nothing until applied to a request"]
pub struct Condition<'a>(ConditionInner<'a>);
impl<'a> Condition<'a> {
pub fn cmp(
attr: impl Into<Cow<'a, str>>,
cmp: Comparison,
value: impl IntoAttributeValue,
) -> Self {
Self(ConditionInner::Compare {
attr: attr.into(),
cmp,
value: value.into_attribute_value(),
})
}
pub fn eq(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self::cmp(attr, Comparison::Eq, value)
}
pub fn ne(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self::cmp(attr, Comparison::Neq, value)
}
pub fn lt(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self::cmp(attr, Comparison::Lt, value)
}
pub fn le(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self::cmp(attr, Comparison::Le, value)
}
pub fn gt(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self::cmp(attr, Comparison::Gt, value)
}
pub fn ge(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self::cmp(attr, Comparison::Ge, value)
}
pub fn between(
attr: impl Into<Cow<'a, str>>,
low: impl IntoAttributeValue,
high: impl IntoAttributeValue,
) -> Self {
Self(ConditionInner::Between {
attr: attr.into(),
low: low.into_attribute_value(),
high: high.into_attribute_value(),
})
}
pub fn is_in(
attr: impl Into<Cow<'a, str>>,
values: impl IntoIterator<Item = impl IntoAttributeValue>,
) -> Self {
Self(ConditionInner::In {
attr: attr.into(),
values: values
.into_iter()
.map(IntoAttributeValue::into_attribute_value)
.collect(),
})
}
pub fn exists(attr: impl Into<Cow<'a, str>>) -> Self {
Self(ConditionInner::Exists(attr.into()))
}
pub fn not_exists(attr: impl Into<Cow<'a, str>>) -> Self {
Self(ConditionInner::NotExists(attr.into()))
}
pub fn begins_with(attr: impl Into<Cow<'a, str>>, prefix: impl IntoAttributeValue) -> Self {
Self(ConditionInner::BeginsWith {
attr: attr.into(),
prefix: prefix.into_attribute_value(),
})
}
pub fn contains(attr: impl Into<Cow<'a, str>>, value: impl IntoAttributeValue) -> Self {
Self(ConditionInner::Contains {
attr: attr.into(),
value: value.into_attribute_value(),
})
}
pub fn size_cmp(attr: impl Into<Cow<'a, str>>, cmp: Comparison, value: usize) -> Self {
Self(ConditionInner::SizeCompare {
attr: attr.into(),
cmp,
value: value.into_attribute_value(),
})
}
pub fn size_eq(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
Self::size_cmp(attr, Comparison::Eq, value)
}
pub fn size_ne(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
Self::size_cmp(attr, Comparison::Neq, value)
}
pub fn size_lt(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
Self::size_cmp(attr, Comparison::Lt, value)
}
pub fn size_le(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
Self::size_cmp(attr, Comparison::Le, value)
}
pub fn size_gt(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
Self::size_cmp(attr, Comparison::Gt, value)
}
pub fn size_ge(attr: impl Into<Cow<'a, str>>, value: usize) -> Self {
Self::size_cmp(attr, Comparison::Ge, value)
}
pub fn and(conditions: impl IntoIterator<Item = Condition<'a>>) -> Self {
let iterator = conditions.into_iter();
let est_size = iterator.size_hint().0;
Self(ConditionInner::And(iterator.fold(
Vec::with_capacity(est_size),
|mut conditions, c| {
match c.0 {
ConditionInner::And(conds) => {
conditions.extend(conds);
}
_ => {
conditions.push(c);
}
};
conditions
},
)))
}
pub fn or(conditions: impl IntoIterator<Item = Condition<'a>>) -> Self {
let iterator = conditions.into_iter();
let est_size = iterator.size_hint().0;
Self(ConditionInner::Or(iterator.fold(
Vec::with_capacity(est_size),
|mut conditions, c| {
match c.0 {
ConditionInner::Or(conds) => {
conditions.extend(conds);
}
_ => {
conditions.push(c);
}
};
conditions
},
)))
}
}
impl<'a> core::ops::Not for Condition<'a> {
type Output = Condition<'a>;
fn not(self) -> Self::Output {
match self.0 {
ConditionInner::Not(condition) => *condition,
_ => Self(ConditionInner::Not(Box::new(self))),
}
}
}
impl<'a> core::ops::BitAnd for Condition<'a> {
type Output = Condition<'a>;
fn bitand(self, rhs: Self) -> Self::Output {
Self::and([self, rhs])
}
}
impl<'a> core::ops::BitOr for Condition<'a> {
type Output = Condition<'a>;
fn bitor(self, rhs: Self) -> Self::Output {
Self::or([self, rhs])
}
}
#[derive(Debug, Default)]
struct BuiltCondition {
expression: Expression,
names: AttrNames,
values: AttrValues,
}
impl BuiltCondition {
const EMPTY: Self = Self {
expression: String::new(),
names: vec![],
values: vec![],
};
}
impl Condition<'_> {
fn build(self) -> BuiltCondition {
self.build_with_counter(&mut 0)
}
fn build_with_counter(self, counter: &mut usize) -> BuiltCondition {
match self.0 {
ConditionInner::And(conditions) => Self::build_logical(conditions, " AND ", counter),
ConditionInner::Or(conditions) => Self::build_logical(conditions, " OR ", counter),
ConditionInner::Not(inner) => {
let mut built = inner.build_with_counter(counter);
if !built.expression.is_empty() {
built.expression = format!("(NOT {})", built.expression);
}
built
}
ConditionInner::Compare { attr, cmp, value } => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
let val_id = *counter;
*counter += 1;
let val_ph = format!(":c{val_id}");
BuiltCondition {
expression: format!("{attr_expr} {cmp} {val_ph}"),
names,
values: vec![(val_ph, value)],
}
}
ConditionInner::Between { attr, low, high } => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
let val_id = *counter;
*counter += 1;
let lo_ph = format!(":c{val_id}lo");
let hi_ph = format!(":c{val_id}hi");
BuiltCondition {
expression: format!("{attr_expr} BETWEEN {lo_ph} AND {hi_ph}"),
names,
values: vec![(lo_ph, low), (hi_ph, high)],
}
}
ConditionInner::In { attr, values } => {
if values.is_empty() {
BuiltCondition::EMPTY
} else {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
let val_id = *counter;
*counter += 1;
let val_phs: Vec<String> = (0..values.len())
.map(|i| format!(":c{val_id}i{i}"))
.collect();
let in_list = val_phs.join(", ");
BuiltCondition {
expression: format!("{attr_expr} IN ({in_list})"),
names,
values: val_phs.into_iter().zip(values.iter().cloned()).collect(),
}
}
}
ConditionInner::Exists(attr) => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
BuiltCondition {
expression: format!("attribute_exists({attr_expr})"),
names,
values: vec![],
}
}
ConditionInner::NotExists(attr) => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
BuiltCondition {
expression: format!("attribute_not_exists({attr_expr})"),
names,
values: vec![],
}
}
ConditionInner::BeginsWith { attr, prefix } => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
let val_id = *counter;
*counter += 1;
let prefix_ph = format!(":c{val_id}");
BuiltCondition {
expression: format!("begins_with({attr_expr}, {prefix_ph})"),
names,
values: vec![(prefix_ph, prefix)],
}
}
ConditionInner::Contains { attr, value } => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
let val_id = *counter;
*counter += 1;
let val_ph = format!(":c{val_id}");
BuiltCondition {
expression: format!("contains({attr_expr}, {val_ph})"),
names,
values: vec![(val_ph, value)],
}
}
ConditionInner::SizeCompare { attr, cmp, value } => {
let (attr_expr, names) = resolve_attr_path(&attr, "c", counter);
let val_id = *counter;
*counter += 1;
let val_ph = format!(":c{val_id}");
BuiltCondition {
expression: format!("size({attr_expr}) {cmp} {val_ph}"),
names,
values: vec![(val_ph, value)],
}
}
}
}
fn build_logical(
conditions: Vec<Condition>,
operator: &str,
counter: &mut usize,
) -> BuiltCondition {
let mut result = BuiltCondition::default();
let mut parts = Vec::with_capacity(conditions.len());
for cond in conditions {
let BuiltCondition {
expression,
names,
values,
} = cond.build_with_counter(counter);
if !expression.is_empty() {
parts.push(expression);
result.names.extend(names);
result.values.extend(values);
}
}
result.expression = match parts.len() {
0 => String::new(),
1 => parts.pop().expect("parts has exactly one element"),
_ => {
let joined = parts.join(operator);
format!("({joined})")
}
};
result
}
}
impl fmt::Display for Condition<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let built = self.clone().build();
if built.expression.is_empty() {
return f.write_str("<none>");
}
if f.alternate() {
f.write_str(&built.expression)?;
fmt_attr_maps(f, &built.names, &built.values)
} else {
f.write_str(&resolve_expression(
&built.expression,
&built.names,
&built.values,
))
}
}
}
impl<B: ConditionableBuilder> ApplyCondition<B> for Condition<'_> {
fn apply(self, builder: B) -> B {
let built = self.build();
if built.expression.is_empty() {
return builder;
}
builder
.condition_expression(built.expression)
.apply_names_and_values(built.names, built.values)
}
}
impl<B: ConditionableBuilder> ApplyCondition<B> for Option<Condition<'_>> {
fn apply(self, builder: B) -> B {
match self {
Some(c) => c.apply(builder),
None => builder,
}
}
}
impl<B: KeyConditionableBuilder> ApplyKeyCondition<B> for Condition<'_> {
fn apply_key_condition(self, builder: B) -> B {
let built = self.build();
if built.expression.is_empty() {
return builder;
}
builder
.key_condition_expression(built.expression)
.apply_names_and_values(built.names, built.values)
}
}
impl<B: FilterableBuilder> ApplyFilter<B> for Condition<'_> {
fn apply_filter(self, builder: B) -> B {
let built = self.build();
if built.expression.is_empty() {
return builder;
}
builder
.filter_expression(built.expression)
.apply_names_and_values(built.names, built.values)
}
}
impl<B: FilterableBuilder> ApplyFilter<B> for Option<Condition<'_>> {
fn apply_filter(self, builder: B) -> B {
match self {
Some(c) => c.apply_filter(builder),
None => builder,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_condition_display_default_simple_eq() {
let c = Condition::eq("PK", "USER#123");
let display = format!("{c}");
assert_eq!(display, r#"PK = S("USER#123")"#);
}
#[test]
fn test_condition_display_default_reserved_word() {
let c = Condition::eq("Status", "active");
let display = format!("{c}");
assert_eq!(display, r#"Status = S("active")"#);
}
#[test]
fn test_condition_display_default_and_with_begins_with() {
let c = Condition::and([
Condition::eq("PK", "USER#123"),
Condition::begins_with("SK", "ORDER#"),
]);
let display = format!("{c}");
assert_eq!(
display,
r#"(PK = S("USER#123") AND begins_with(SK, S("ORDER#")))"#
);
}
#[test]
fn test_condition_display_default_or() {
let c = Condition::or([
Condition::eq("attr1", "value1"),
Condition::ne("attr2", 2345u32),
]);
let display = format!("{c}");
assert_eq!(display, r#"(attr1 = S("value1") OR attr2 <> N("2345"))"#);
}
#[test]
fn test_condition_display_default_not() {
let c = !Condition::exists("PK");
let display = format!("{c}");
assert_eq!(display, "(NOT attribute_exists(PK))");
}
#[test]
fn test_condition_display_default_between() {
let c = Condition::between("age", 18u32, 65u32);
let display = format!("{c}");
assert_eq!(display, r#"age BETWEEN N("18") AND N("65")"#);
}
#[test]
fn test_condition_display_default_in() {
let c = Condition::is_in("color", ["red", "green", "blue"]);
let display = format!("{c}");
assert_eq!(display, r#"color IN (S("red"), S("green"), S("blue"))"#);
}
#[test]
fn test_condition_display_default_size() {
let c = Condition::size_cmp("tags", Comparison::Ge, 3);
let display = format!("{c}");
assert_eq!(display, r#"size(tags) >= N("3")"#);
}
#[test]
fn test_condition_display_default_contains() {
let c = Condition::contains("description", "rust");
let display = format!("{c}");
assert_eq!(display, r#"contains(description, S("rust"))"#);
}
#[test]
fn test_condition_display_default_empty() {
let c = Condition::and([]);
let display = format!("{c}");
assert_eq!(display, "<none>");
}
#[test]
fn test_condition_display_alternate_simple() {
let c = Condition::eq("PK", "USER#123");
let display = format!("{c:#}");
assert_eq!(display, "PK = :c0\n values: { :c0 = S(\"USER#123\") }");
}
#[test]
fn test_condition_display_alternate_reserved_word() {
let c = Condition::eq("Status", "active");
let display = format!("{c:#}");
assert_eq!(
display,
"#c0 = :c1\n names: { #c0 = Status }\n values: { :c1 = S(\"active\") }"
);
}
#[test]
fn test_condition_display_alternate_and_with_begins_with() {
let c = Condition::and([
Condition::eq("PK", "USER#123"),
Condition::begins_with("SK", "ORDER#"),
]);
let display = format!("{c:#}");
assert_eq!(
display,
"(PK = :c0 AND begins_with(SK, :c1))\n values: { :c0 = S(\"USER#123\"), :c1 = S(\"ORDER#\") }"
);
}
#[test]
fn test_condition_display_alternate_no_values() {
let c = Condition::exists("PK");
let display = format!("{c:#}");
assert_eq!(display, "attribute_exists(PK)");
}
#[test]
fn test_condition_display_alternate_empty() {
let c = Condition::and([]);
let display = format!("{c:#}");
assert_eq!(display, "<none>");
}
}