use crate::errors::{DynoxideError, Result};
use crate::types::AttributeValue;
use std::collections::HashMap;
pub(crate) const MESSAGE: &str = "Empty attribute name";
pub(crate) fn value_has_empty_name(value: &AttributeValue) -> bool {
match value {
AttributeValue::M(map) => map_has_empty_name(map),
AttributeValue::L(list) => list.iter().any(value_has_empty_name),
_ => false,
}
}
pub(crate) fn map_has_empty_name(map: &HashMap<String, AttributeValue>) -> bool {
map.iter()
.any(|(name, value)| name.is_empty() || value_has_empty_name(value))
}
pub(crate) fn json_value_has_empty_name(value: &serde_json::Value) -> bool {
let Some(object) = value.as_object() else {
return false;
};
if let Some(map) = object.get("M").and_then(|m| m.as_object()) {
return map
.iter()
.any(|(name, inner)| name.is_empty() || json_value_has_empty_name(inner));
}
if let Some(list) = object.get("L").and_then(|l| l.as_array()) {
return list.iter().any(json_value_has_empty_name);
}
false
}
pub(crate) fn json_map_has_empty_name(value: &serde_json::Value) -> bool {
value.as_object().is_some_and(|map| {
map.iter()
.any(|(name, inner)| name.is_empty() || json_value_has_empty_name(inner))
})
}
pub(crate) fn empty_placeholder(names: Option<&HashMap<String, String>>) -> Option<&str> {
let names = names?;
let mut empty: Vec<&str> = names
.iter()
.filter(|(_, name)| name.is_empty())
.map(|(placeholder, _)| placeholder.as_str())
.collect();
empty.sort_unstable();
empty.first().copied()
}
pub(crate) fn placeholder_message(placeholder: &str) -> String {
format!("ExpressionAttributeNames contains invalid value: {MESSAGE} for key {placeholder}")
}
pub(crate) fn check_names(names: Option<&HashMap<String, String>>) -> Result<()> {
match empty_placeholder(names) {
Some(placeholder) => Err(DynoxideError::ValidationException(placeholder_message(
placeholder,
))),
None => Ok(()),
}
}
pub(crate) fn value_placeholder<'a>(
values: Option<&'a HashMap<String, AttributeValue>>,
raw: Option<&'a serde_json::Value>,
) -> Option<&'a str> {
let mut found: Vec<&str> = match (raw, values) {
(Some(raw), _) => raw
.as_object()
.map(|map| {
map.iter()
.filter(|(_, value)| json_value_has_empty_name(value))
.map(|(key, _)| key.as_str())
.collect()
})
.unwrap_or_default(),
(None, Some(values)) => values
.iter()
.filter(|(_, value)| value_has_empty_name(value))
.map(|(key, _)| key.as_str())
.collect(),
(None, None) => Vec::new(),
};
found.sort_unstable();
found.first().copied()
}
pub(crate) fn value_placeholder_message(key: &str) -> String {
format!("ExpressionAttributeValues contains invalid value: {MESSAGE} for key {key}")
}
pub(crate) fn names_include_empty(names: Option<&Vec<String>>) -> bool {
names.is_some_and(|names| names.iter().any(String::is_empty))
}
pub(crate) fn expected_has_empty_name(expected: Option<&serde_json::Value>) -> bool {
let Some(map) = expected.and_then(|e| e.as_object()) else {
return false;
};
map.iter()
.any(|(name, condition)| name.is_empty() || condition_values_have_empty_name(condition))
}
pub(crate) fn filter_values_have_empty_name(filter: Option<&serde_json::Value>) -> bool {
filter
.and_then(|f| f.as_object())
.is_some_and(|map| map.values().any(condition_values_have_empty_name))
}
fn condition_values_have_empty_name(condition: &serde_json::Value) -> bool {
let Some(condition) = condition.as_object() else {
return false;
};
condition
.get("Value")
.is_some_and(json_value_has_empty_name)
|| condition
.get("AttributeValueList")
.and_then(|l| l.as_array())
.is_some_and(|list| list.iter().any(json_value_has_empty_name))
}
pub(crate) fn parameters_have_empty_name(parameters: Option<&[AttributeValue]>) -> bool {
parameters.is_some_and(|params| params.iter().any(value_has_empty_name))
}
pub(crate) fn bare() -> DynoxideError {
DynoxideError::ValidationException(MESSAGE.to_string())
}
pub(crate) fn enveloped() -> DynoxideError {
DynoxideError::ValidationException(crate::validation::envelope_message(MESSAGE))
}
pub(crate) fn check_single_item_write(
attributes: &HashMap<String, AttributeValue>,
updates_empty: bool,
expected: Option<&serde_json::Value>,
names: Option<&HashMap<String, String>>,
values: Option<&HashMap<String, AttributeValue>>,
raw_values: Option<&serde_json::Value>,
) -> Result<()> {
if map_has_empty_name(attributes) || updates_empty || expected_has_empty_name(expected) {
return Err(enveloped());
}
if let Some(placeholder) = empty_placeholder(names) {
return Err(DynoxideError::ValidationException(
crate::validation::envelope_message(&placeholder_message(placeholder)),
));
}
if value_placeholder(values, raw_values).is_some() {
return Err(enveloped());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn map(entries: &[(&str, AttributeValue)]) -> HashMap<String, AttributeValue> {
entries
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[test]
fn finds_an_empty_name_at_any_depth() {
let leaf = AttributeValue::S("x".into());
assert!(map_has_empty_name(&map(&[("", leaf.clone())])));
let nested =
AttributeValue::M(map(&[("a", AttributeValue::M(map(&[("", leaf.clone())])))]));
assert!(map_has_empty_name(&map(&[("m", nested)])));
let in_list = AttributeValue::L(vec![AttributeValue::M(map(&[("", leaf.clone())]))]);
assert!(map_has_empty_name(&map(&[("l", in_list)])));
assert!(!map_has_empty_name(&map(&[(" ", leaf.clone())])));
assert!(!map_has_empty_name(&map(&[(
"s",
AttributeValue::S(String::new())
)])));
}
#[test]
fn reads_unparsed_values_the_same_way() {
assert!(json_value_has_empty_name(
&json!({"M": {"a": {"L": [{"M": {"": {"N": "1"}}}]}}})
));
assert!(!json_value_has_empty_name(&json!({"M": {" ": {"S": ""}}})));
assert!(json_map_has_empty_name(
&json!({"pk": {"S": "a"}, "": {"S": "x"}})
));
}
#[test]
fn legacy_filters_check_values_but_accept_an_empty_name() {
let named = json!({"": {"ComparisonOperator": "NOT_NULL"}});
assert!(!filter_values_have_empty_name(Some(&named)));
let valued = json!({"m": {"ComparisonOperator": "EQ", "AttributeValueList": [{"M": {"": {"S": "x"}}}]}});
assert!(filter_values_have_empty_name(Some(&valued)));
let expected_name = json!({"": {"Exists": false}});
assert!(expected_has_empty_name(Some(&expected_name)));
let expected_value = json!({"x": {"Value": {"M": {"": {"S": "x"}}}}});
assert!(expected_has_empty_name(Some(&expected_value)));
}
#[test]
fn the_raw_values_are_read_when_a_request_carries_both() {
let parsed = map(&[(":v", AttributeValue::S("clean".into()))]);
let raw = json!({":v": {"M": {"": {"S": "x"}}}});
assert_eq!(value_placeholder(Some(&parsed), Some(&raw)), Some(":v"));
assert_eq!(value_placeholder(Some(&parsed), None), None);
}
}