use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use serde::de;
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt;
#[derive(Clone, PartialEq)]
pub enum AttributeValue {
S(String),
N(String),
B(Vec<u8>),
BOOL(bool),
NULL(bool),
SS(Vec<String>),
NS(Vec<String>),
BS(Vec<Vec<u8>>),
L(Vec<AttributeValue>),
M(HashMap<String, AttributeValue>),
}
impl fmt::Debug for AttributeValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AttributeValue::S(v) => f.debug_tuple("S").field(v).finish(),
AttributeValue::N(v) => f.debug_tuple("N").field(v).finish(),
AttributeValue::B(v) => f.debug_tuple("B").field(v).finish(),
AttributeValue::BOOL(v) => f.debug_tuple("BOOL").field(v).finish(),
AttributeValue::NULL(v) => f.debug_tuple("NULL").field(v).finish(),
AttributeValue::SS(v) => f.debug_tuple("SS").field(v).finish(),
AttributeValue::NS(v) => f.debug_tuple("NS").field(v).finish(),
AttributeValue::BS(v) => f.debug_tuple("BS").field(v).finish(),
AttributeValue::L(v) => f.debug_tuple("L").field(v).finish(),
AttributeValue::M(map) => {
struct Sorted<'a>(&'a HashMap<String, AttributeValue>);
impl fmt::Debug for Sorted<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut entries: Vec<_> = self.0.iter().collect();
entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
f.debug_map().entries(entries).finish()
}
}
f.debug_tuple("M").field(&Sorted(map)).finish()
}
}
}
}
impl AttributeValue {
pub fn size(&self) -> usize {
match self {
AttributeValue::S(s) => s.len(),
AttributeValue::N(n) => number_size(n),
AttributeValue::B(b) => b.len(),
AttributeValue::BOOL(_) => 1,
AttributeValue::NULL(_) => 1,
AttributeValue::SS(ss) => ss.iter().map(|s| s.len()).sum(),
AttributeValue::NS(ns) => ns.iter().map(|n| number_size(n)).sum(),
AttributeValue::BS(bs) => bs.iter().map(|b| b.len()).sum(),
AttributeValue::L(items) => {
3 + items.len() + items.iter().map(|v| v.size()).sum::<usize>()
}
AttributeValue::M(map) => {
3 + map
.iter()
.map(|(k, v)| k.len() + 1 + v.size())
.sum::<usize>()
}
}
}
pub fn type_name(&self) -> &'static str {
match self {
AttributeValue::S(_) => "S",
AttributeValue::N(_) => "N",
AttributeValue::B(_) => "B",
AttributeValue::BOOL(_) => "BOOL",
AttributeValue::NULL(_) => "NULL",
AttributeValue::SS(_) => "SS",
AttributeValue::NS(_) => "NS",
AttributeValue::BS(_) => "BS",
AttributeValue::L(_) => "L",
AttributeValue::M(_) => "M",
}
}
pub fn is_scalar(&self) -> bool {
matches!(
self,
AttributeValue::S(_)
| AttributeValue::N(_)
| AttributeValue::B(_)
| AttributeValue::BOOL(_)
| AttributeValue::NULL(_)
)
}
pub fn is_set(&self) -> bool {
matches!(
self,
AttributeValue::SS(_) | AttributeValue::NS(_) | AttributeValue::BS(_)
)
}
pub fn to_key_string(&self) -> Option<String> {
match self {
AttributeValue::S(s) => Some(format!("S:{s}")),
AttributeValue::N(n) => Some(format!("N:{}", normalize_number_for_sort(n))),
AttributeValue::B(b) => Some(format!("B:{}", hex_encode(b))),
_ => None, }
}
}
impl fmt::Display for AttributeValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AttributeValue::S(s) => write!(f, "\"{s}\""),
AttributeValue::N(n) => write!(f, "{n}"),
AttributeValue::B(b) => write!(f, "<binary {} bytes>", b.len()),
AttributeValue::BOOL(b) => write!(f, "{b}"),
AttributeValue::NULL(_) => write!(f, "null"),
AttributeValue::SS(ss) => write!(f, "{ss:?}"),
AttributeValue::NS(ns) => write!(f, "{ns:?}"),
AttributeValue::BS(bs) => write!(f, "<binary set {} items>", bs.len()),
AttributeValue::L(items) => write!(f, "<list {} items>", items.len()),
AttributeValue::M(map) => write!(f, "<map {} keys>", map.len()),
}
}
}
impl Serialize for AttributeValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(1))?;
match self {
AttributeValue::S(s) => map.serialize_entry("S", s)?,
AttributeValue::N(n) => map.serialize_entry("N", n)?,
AttributeValue::B(b) => {
map.serialize_entry("B", &BASE64.encode(b))?;
}
AttributeValue::BOOL(b) => map.serialize_entry("BOOL", b)?,
AttributeValue::NULL(n) => map.serialize_entry("NULL", n)?,
AttributeValue::SS(ss) => {
map.serialize_entry("SS", &crate::set_order::sorted_strings(ss))?
}
AttributeValue::NS(ns) => {
map.serialize_entry("NS", &crate::set_order::sorted_numbers(ns))?
}
AttributeValue::BS(bs) => {
let encoded: Vec<String> = crate::set_order::sorted_binaries(bs)
.into_iter()
.map(|b| BASE64.encode(b))
.collect();
map.serialize_entry("BS", &encoded)?;
}
AttributeValue::L(items) => map.serialize_entry("L", items)?,
AttributeValue::M(m) => map.serialize_entry("M", m)?,
}
map.end()
}
}
impl<'de> Deserialize<'de> for AttributeValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = serde_json::Value::deserialize(deserializer)?;
let obj = raw
.as_object()
.ok_or_else(|| de::Error::custom("empty AttributeValue object"))?;
if obj.is_empty() {
return Err(de::Error::custom("empty AttributeValue object"));
}
let present: Vec<&str> = obj
.iter()
.filter(|(k, v)| TYPE_MEMBERS.contains(&k.as_str()) && !v.is_null())
.map(|(k, _)| k.as_str())
.collect();
if present.is_empty() {
return Err(de::Error::custom(
"Supplied AttributeValue is empty, must contain exactly one of the supported datatypes",
));
}
let boolean = match obj.get("BOOL").filter(|v| !v.is_null()) {
Some(v) => Some(read_lenient_bool(v).map_err(de::Error::custom)?),
None => None,
};
let null = match obj.get("NULL").filter(|v| !v.is_null()) {
Some(v) => Some(read_lenient_bool(v).map_err(de::Error::custom)?),
None => None,
};
let layer = if present.len() > 1 {
crate::validation::ValueLayer::Older
} else {
current_value_layer()
};
if present.len() > 1
&& let Some(refusal) = present
.iter()
.find_map(|&type_key| empty_set_refusal(type_key, &obj[type_key]))
{
return Err(de::Error::custom(format!(
"{}{refusal}",
crate::serde_errors::VALIDATION_MARKER
)));
}
for &type_key in &present {
match type_key {
"N" => {
if let Some(n) = obj.get("N").and_then(|v| v.as_str()) {
validate_number_in_deser(n, layer).map_err(de::Error::custom)?;
}
}
"NS" => {
if let Some(arr) = obj.get("NS").and_then(|v| v.as_array()) {
for item in arr {
if let Some(n) = item.as_str() {
validate_number_in_deser(n, layer).map_err(de::Error::custom)?;
}
}
}
}
_ => {}
}
}
if let Some(LenientBool {
value: false,
from_string,
}) = null
{
let marker = if from_string || layer == crate::validation::ValueLayer::Older {
crate::serde_errors::VALIDATION_MARKER
} else {
crate::serde_errors::REQUEST_VALIDATION_MARKER
};
return Err(de::Error::custom(format!(
"{marker}{}",
crate::serde_errors::NULL_NOT_TRUE
)));
}
if present.len() > 1 {
return Err(de::Error::custom(
"VALIDATION:Supplied AttributeValue has more than one datatypes set, \
must contain exactly one of the supported datatypes",
));
}
let type_key = present[0];
let val = &obj[type_key];
match type_key {
"S" => {
let s = val
.as_str()
.ok_or_else(|| de::Error::custom("expected string for S"))?;
Ok(AttributeValue::S(s.to_string()))
}
"N" => {
let n = val
.as_str()
.ok_or_else(|| de::Error::custom("expected string for N"))?;
Ok(AttributeValue::N(n.to_string()))
}
"B" => {
let encoded = val
.as_str()
.ok_or_else(|| de::Error::custom("expected string for B"))?;
let bytes = BASE64
.decode(encoded)
.map_err(|e| de::Error::custom(format!("invalid base64: {e}")))?;
Ok(AttributeValue::B(bytes))
}
"BOOL" => Ok(AttributeValue::BOOL(boolean.is_some_and(|b| b.value))),
"NULL" => Ok(AttributeValue::NULL(true)),
"SS" => {
let arr = val
.as_array()
.ok_or_else(|| de::Error::custom("expected array for SS"))?;
let ss: Result<Vec<String>, _> = arr
.iter()
.map(|v| {
v.as_str()
.map(|s| s.to_string())
.ok_or_else(|| de::Error::custom("expected string in SS"))
})
.collect();
Ok(AttributeValue::SS(ss?))
}
"NS" => {
let arr = val
.as_array()
.ok_or_else(|| de::Error::custom("expected array for NS"))?;
let ns: Result<Vec<String>, _> = arr
.iter()
.map(|v| {
v.as_str()
.map(|s| s.to_string())
.ok_or_else(|| de::Error::custom("expected string in NS"))
})
.collect();
Ok(AttributeValue::NS(ns?))
}
"BS" => {
let arr = val
.as_array()
.ok_or_else(|| de::Error::custom("expected array for BS"))?;
let mut decoded = Vec::with_capacity(arr.len());
for item in arr {
let encoded = item
.as_str()
.ok_or_else(|| de::Error::custom("expected string in BS"))?;
decoded.push(
BASE64
.decode(encoded)
.map_err(|e| de::Error::custom(format!("invalid base64: {e}")))?,
);
}
Ok(AttributeValue::BS(decoded))
}
"L" => {
let arr = val
.as_array()
.ok_or_else(|| de::Error::custom("expected array for L"))?;
let list: Result<Vec<AttributeValue>, _> = arr
.iter()
.map(|v| serde_json::from_value(v.clone()).map_err(de::Error::custom))
.collect();
Ok(AttributeValue::L(list?))
}
"M" => {
let map_val = val
.as_object()
.ok_or_else(|| de::Error::custom("expected object for M"))?;
let mut result = std::collections::HashMap::new();
for (k, v) in map_val {
let av: AttributeValue =
serde_json::from_value(v.clone()).map_err(de::Error::custom)?;
result.insert(k.clone(), av);
}
Ok(AttributeValue::M(result))
}
_ => unreachable!(),
}
}
}
#[derive(Clone, Copy)]
struct LenientBool {
value: bool,
from_string: bool,
}
fn read_lenient_bool(value: &serde_json::Value) -> Result<LenientBool, String> {
let refused = |message: &str| format!("{}{message}", crate::serde_errors::SERIALIZATION_MARKER);
match value {
serde_json::Value::Bool(value) => Ok(LenientBool {
value: *value,
from_string: false,
}),
serde_json::Value::String(text) => match lenient_bool_text(text) {
Some(value) => Ok(LenientBool {
value,
from_string: true,
}),
None => Err(refused("Unexpected token received from parser")),
},
serde_json::Value::Number(n) if n.is_f64() => {
Err(refused("DECIMAL_VALUE cannot be converted to Boolean"))
}
serde_json::Value::Number(_) => Err(refused("NUMBER_VALUE cannot be converted to Boolean")),
serde_json::Value::Array(_) => Err(refused("")),
serde_json::Value::Object(_) => Err(refused(
"Start of structure or map found where not expected",
)),
serde_json::Value::Null => Err(refused("Unexpected token received from parser")),
}
}
pub(crate) fn deserialize_lenient_flag<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<bool>, D::Error> {
match serde_json::Value::deserialize(deserializer)? {
serde_json::Value::Null => Ok(None),
serde_json::Value::Array(_) => Err(de::Error::custom(format!(
"{}Unrecognized collection type class java.lang.Boolean",
crate::serde_errors::SERIALIZATION_MARKER
))),
value => read_lenient_bool(&value)
.map(|flag| Some(flag.value))
.map_err(de::Error::custom),
}
}
pub(crate) fn malformed_condition_boolean(
conditions: Option<&serde_json::Value>,
) -> Option<String> {
let malformed = |message: String| {
message
.starts_with(crate::serde_errors::SERIALIZATION_MARKER)
.then_some(message)
};
for condition in conditions?.as_object()?.values() {
let Some(condition) = condition.as_object() else {
continue;
};
if let Some(exists) = condition.get("Exists")
&& let Err(e) = deserialize_lenient_flag(exists.clone())
&& let Some(message) = malformed(e.to_string())
{
return Some(message);
}
let values = condition.get("Value").into_iter().chain(
condition
.get("AttributeValueList")
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten(),
);
for value in values {
if let Err(e) = serde_json::from_value::<AttributeValue>(value.clone())
&& let Some(message) = malformed(e.to_string())
{
return Some(message);
}
}
}
None
}
#[cfg(any(feature = "http-server", feature = "wasm-sqlite", test))]
pub(crate) fn reads_as_flag(text: &str) -> bool {
lenient_bool_text(text).is_some()
}
fn lenient_bool_text(text: &str) -> Option<bool> {
match text.to_ascii_lowercase().as_str() {
"true" | "t" | "yes" | "y" | "1" => Some(true),
"false" | "f" | "no" | "n" | "0" => Some(false),
_ => None,
}
}
fn validate_number_in_deser(n: &str, layer: crate::validation::ValueLayer) -> Result<(), String> {
let marker = match layer {
crate::validation::ValueLayer::Current => crate::serde_errors::REQUEST_VALIDATION_MARKER,
crate::validation::ValueLayer::Older => crate::serde_errors::VALIDATION_MARKER,
};
match layer.check_number(n) {
Ok(()) => Ok(()),
Err(crate::errors::DynoxideError::ValidationException(m)) => Err(format!("{marker}{m}")),
Err(e) => Err(format!("{marker}{e}")),
}
}
pub(crate) fn validate_current_number(
num_str: &str,
) -> std::result::Result<(), crate::errors::DynoxideError> {
if num_str.is_empty() {
return Err(crate::errors::DynoxideError::ValidationException(
"The parameter cannot be converted to a numeric value: ".to_string(),
));
}
validate_dynamo_number(num_str)
}
pub fn normalize_number_for_sort(num_str: &str) -> String {
let trimmed = num_str.trim();
if trimmed.is_empty() || trimmed == "0" || trimmed == "-0" || trimmed == "0.0" {
return zero_encoding();
}
let negative = trimmed.starts_with('-');
let abs_str = if negative { &trimmed[1..] } else { trimmed };
let (mantissa_digits, exponent) = parse_number_parts(abs_str);
if mantissa_digits.is_empty() || mantissa_digits.iter().all(|&d| d == 0) {
return zero_encoding();
}
if negative {
encode_negative(&mantissa_digits, exponent)
} else {
encode_positive(&mantissa_digits, exponent)
}
}
fn empty_set_refusal(type_key: &str, member: &serde_json::Value) -> Option<&'static str> {
if !member.as_array().is_some_and(Vec::is_empty) {
return None;
}
match type_key {
"SS" => Some("One or more parameter values were invalid: An string set may not be empty"),
"NS" => Some("One or more parameter values were invalid: An number set may not be empty"),
"BS" => Some("One or more parameter values were invalid: Binary sets should not be empty"),
_ => None,
}
}
const TYPE_MEMBERS: [&str; 10] = ["S", "N", "B", "BOOL", "NULL", "SS", "NS", "BS", "L", "M"];
pub(crate) fn reads_on_older_layer(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Object(map) => {
let string_flag = ["BOOL", "NULL", "Exists"]
.iter()
.any(|member| map.get(*member).is_some_and(serde_json::Value::is_string));
let members: Vec<&serde_json::Value> = TYPE_MEMBERS
.iter()
.filter_map(|member| map.get(*member))
.filter(|member| !member.is_null())
.collect();
string_flag
|| (members.len() > 1 && members.iter().any(|member| !member.is_object()))
|| map.values().any(reads_on_older_layer)
}
serde_json::Value::Array(items) => items.iter().any(reads_on_older_layer),
_ => false,
}
}
pub(crate) fn current_value_layer() -> crate::validation::ValueLayer {
VALUE_LAYER.with(std::cell::Cell::get)
}
thread_local! {
static VALUE_LAYER: std::cell::Cell<crate::validation::ValueLayer> =
const { std::cell::Cell::new(crate::validation::ValueLayer::Current) };
}
pub(crate) fn with_value_layer<T>(
layer: crate::validation::ValueLayer,
read: impl FnOnce() -> T,
) -> T {
struct Restore(crate::validation::ValueLayer);
impl Drop for Restore {
fn drop(&mut self) {
VALUE_LAYER.with(|cell| cell.set(self.0));
}
}
let _restore = Restore(VALUE_LAYER.with(|cell| cell.replace(layer)));
read()
}
#[cfg(any(feature = "wasm-sqlite", test))]
pub(crate) async fn with_value_layer_async<F: std::future::Future>(
layer: crate::validation::ValueLayer,
future: F,
) -> F::Output {
struct Layered<F> {
layer: crate::validation::ValueLayer,
future: std::pin::Pin<Box<F>>,
}
impl<F: std::future::Future> std::future::Future for Layered<F> {
type Output = F::Output;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<F::Output> {
let layer = self.layer;
with_value_layer(layer, || self.future.as_mut().poll(cx))
}
}
Layered {
layer,
future: Box::pin(future),
}
.await
}
pub(crate) fn reading_parameters<T>(read: impl FnOnce() -> T) -> T {
with_value_layer(crate::validation::ValueLayer::Older, read)
}
pub(crate) fn deserialize_older_expression_values<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<HashMap<String, AttributeValue>>, D::Error> {
let Some(raw) =
Option::<serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?
else {
return Ok(None);
};
let mut values = HashMap::with_capacity(raw.len());
for (key, value) in raw {
let read = with_value_layer(crate::validation::ValueLayer::Older, || {
serde_json::from_value::<AttributeValue>(value)
});
match read {
Ok(av) => {
values.insert(key, av);
}
Err(e) => {
use crate::serde_errors::{ValueRefusal, named_value_refusal, value_refusal};
let msg = e.to_string();
return Err(de::Error::custom(match value_refusal(&msg) {
ValueRefusal::Number(inner) => format!(
"{}{}",
crate::serde_errors::VALIDATION_MARKER,
named_value_refusal(inner, &key)
),
_ => msg,
}));
}
}
}
Ok(Some(values))
}
pub(crate) fn deserialize_named_null_refusals<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<HashMap<String, AttributeValue>>, D::Error> {
deserialize_named_refusals(deserializer, false, true)
}
pub(crate) fn deserialize_named_empty_and_null_refusals<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<HashMap<String, AttributeValue>>, D::Error> {
deserialize_named_refusals(deserializer, true, false)
}
fn deserialize_named_refusals<'de, D: serde::Deserializer<'de>>(
deserializer: D,
name_empty: bool,
name_older: bool,
) -> std::result::Result<Option<HashMap<String, AttributeValue>>, D::Error> {
let Some(raw) =
Option::<serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?
else {
return Ok(None);
};
let mut values = HashMap::with_capacity(raw.len());
for (key, value) in raw {
match serde_json::from_value::<AttributeValue>(value) {
Ok(av) => {
values.insert(key, av);
}
Err(e) => {
use crate::serde_errors::{
EMPTY_VALUE, MORE_THAN_ONE_TYPE, VALIDATION_MARKER, ValueRefusal,
named_value_refusal, value_refusal,
};
let msg = e.to_string();
let older =
name_older && current_value_layer() == crate::validation::ValueLayer::Older;
return Err(de::Error::custom(match value_refusal(&msg) {
ValueRefusal::ParsedNull(inner) => {
format!("{VALIDATION_MARKER}{}", named_value_refusal(inner, &key))
}
ValueRefusal::Empty if name_empty => {
format!(
"{VALIDATION_MARKER}{}",
named_value_refusal(EMPTY_VALUE, &key)
)
}
ValueRefusal::MoreThanOneType if name_older => format!(
"{VALIDATION_MARKER}{}",
named_value_refusal(MORE_THAN_ONE_TYPE, &key)
),
ValueRefusal::EmptySet(inner) if name_older => {
format!("{VALIDATION_MARKER}{}", named_value_refusal(inner, &key))
}
ValueRefusal::Number(inner) if older => {
format!("{VALIDATION_MARKER}{}", named_value_refusal(inner, &key))
}
_ => msg,
}));
}
}
}
Ok(Some(values))
}
pub(crate) fn deserialize_parameters<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<Vec<AttributeValue>>, D::Error> {
reading_parameters(|| Option::<Vec<AttributeValue>>::deserialize(deserializer))
}
pub(crate) fn validate_parameter_number(
num_str: &str,
) -> std::result::Result<(), crate::errors::DynoxideError> {
if num_str.is_empty() || !is_well_formed_dynamo_number(num_str) {
return validate_dynamo_number(num_str);
}
let unconvertible = || {
crate::errors::DynoxideError::ValidationException(format!(
"The parameter cannot be converted to a numeric value: {num_str}"
))
};
let unsigned = num_str.trim_start_matches(['+', '-']);
let (coefficient, exponent) = match unsigned.find(['e', 'E']) {
Some(at) => (
&unsigned[..at],
unsigned[at + 1..]
.parse::<i32>()
.map_err(|_| unconvertible())?,
),
None => (unsigned, 0),
};
let fraction_digits = coefficient
.split_once('.')
.map_or(0, |(_, fraction)| fraction.len());
let scale = i64::try_from(fraction_digits).unwrap_or(i64::MAX) - i64::from(exponent);
if i32::try_from(scale).is_err() {
return Err(unconvertible());
}
let significant = coefficient
.chars()
.filter(char::is_ascii_digit)
.skip_while(|&d| d == '0')
.count()
.max(1);
if i64::try_from(significant).unwrap_or(i64::MAX) - scale >= i64::from(i32::MAX) {
return Err(crate::errors::DynoxideError::ValidationException(
"Number underflow. Attempting to store a number with magnitude smaller than \
supported range"
.to_string(),
));
}
validate_dynamo_number(num_str)
}
pub fn validate_dynamo_number(
num_str: &str,
) -> std::result::Result<(), crate::errors::DynoxideError> {
if num_str.is_empty() {
return Err(crate::errors::DynoxideError::ValidationException(
"The parameter cannot be converted to a numeric value".to_string(),
));
}
if !is_well_formed_dynamo_number(num_str) {
return Err(crate::errors::DynoxideError::ValidationException(format!(
"The parameter cannot be converted to a numeric value: {num_str}"
)));
}
if num_str
.find(['e', 'E'])
.is_some_and(|pos| num_str[pos + 1..].parse::<i64>().is_err())
{
return Err(crate::errors::DynoxideError::ValidationException(format!(
"The parameter cannot be converted to a numeric value: {num_str}"
)));
}
let (mantissa_digits, exponent) = parse_number_parts(num_str);
let exponent = if mantissa_digits.is_empty() || mantissa_digits.iter().all(|&d| d == 0) {
let (coeff, written_exponent) = split_exponent(num_str);
let frac_digits = coeff.find('.').map_or(0, |dot| coeff.len() - dot - 1);
written_exponent
.saturating_sub(frac_digits as i64)
.saturating_add(1)
} else {
if mantissa_digits.len() > 38 {
return Err(crate::errors::DynoxideError::ValidationException(
"Attempting to store more than 38 significant digits in a Number".to_string(),
));
}
i64::from(exponent)
};
if exponent > 126 {
return Err(crate::errors::DynoxideError::ValidationException(
"Number overflow. Attempting to store a number with magnitude larger than supported range"
.to_string(),
));
}
if exponent < -129 {
return Err(crate::errors::DynoxideError::ValidationException(
"Number underflow. Attempting to store a number with magnitude smaller than supported range"
.to_string(),
));
}
Ok(())
}
fn is_well_formed_dynamo_number(s: &str) -> bool {
let bytes = s.as_bytes();
let n = bytes.len();
let mut i = 0;
if i < n && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let mut coeff_digits = 0usize;
let mut dots = 0usize;
while i < n && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
if bytes[i] == b'.' {
dots += 1;
if dots > 1 {
return false;
}
} else {
coeff_digits += 1;
}
i += 1;
}
if coeff_digits == 0 {
return false;
}
if i < n && (bytes[i] == b'e' || bytes[i] == b'E') {
i += 1;
if i < n && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let mut exp_digits = 0usize;
while i < n && bytes[i].is_ascii_digit() {
exp_digits += 1;
i += 1;
}
if exp_digits == 0 {
return false;
}
}
i == n
}
pub fn number_size(num_str: &str) -> usize {
let trimmed = num_str.trim();
let negative = trimmed.starts_with('-');
let abs = trimmed.strip_prefix(['-', '+']).unwrap_or(trimmed);
let (mantissa, exponent) = parse_number_parts(abs);
let int_digits = exponent.clamp(0, mantissa.len() as i32) as usize;
let frac_digits = mantissa.len() - int_digits;
let sign = usize::from(negative && !mantissa.is_empty());
int_digits.div_ceil(2) + frac_digits.div_ceil(2) + 1 + sign
}
pub fn normalize_dynamo_number(num_str: &str) -> String {
let trimmed = num_str.trim();
if trimmed.is_empty() {
return "0".to_string();
}
if validate_dynamo_number(trimmed).is_err() {
return trimmed.to_string();
}
let negative = trimmed.starts_with('-');
let abs_str = if negative {
&trimmed[1..]
} else {
trimmed.trim_start_matches('+')
};
let (mantissa_digits, exponent) = parse_number_parts(abs_str);
if mantissa_digits.is_empty() {
return "0".to_string();
}
let num_digits = mantissa_digits.len() as i32;
let int_digits = exponent;
let mut result = String::new();
if negative {
result.push('-');
}
if int_digits <= 0 {
result.push_str("0.");
for _ in 0..(-int_digits) {
result.push('0');
}
for &d in &mantissa_digits {
result.push((b'0' + d) as char);
}
} else if int_digits >= num_digits {
for &d in &mantissa_digits {
result.push((b'0' + d) as char);
}
for _ in 0..(int_digits - num_digits) {
result.push('0');
}
} else {
let int_part = int_digits as usize;
for &d in &mantissa_digits[..int_part] {
result.push((b'0' + d) as char);
}
result.push('.');
for &d in &mantissa_digits[int_part..] {
result.push((b'0' + d) as char);
}
}
result
}
fn zero_encoding() -> String {
format!("1{}{}", "0".repeat(4), "0".repeat(40))
}
fn encoded_exponent(exponent: i32) -> u16 {
(exponent.clamp(-5000, 4999) + 5000) as u16
}
fn encode_positive(mantissa: &[u8], exponent: i32) -> String {
let exp_encoded = encoded_exponent(exponent);
let mantissa_str = mantissa_to_string(mantissa, 40);
format!("2{exp_encoded:04}{mantissa_str}")
}
fn encode_negative(mantissa: &[u8], exponent: i32) -> String {
let exp_encoded = 9999 - encoded_exponent(exponent);
let mantissa_str = complement_mantissa(mantissa, 40);
format!("0{exp_encoded:04}{mantissa_str}")
}
pub(crate) fn parse_number_parts(s: &str) -> (Vec<u8>, i32) {
let (coeff, exp_part) = split_exponent(s);
let (int_part, frac_part) = if let Some(dot) = coeff.find('.') {
(&coeff[..dot], &coeff[dot + 1..])
} else {
(coeff, "")
};
let mut digits: Vec<u8> = Vec::new();
for ch in int_part.chars().chain(frac_part.chars()) {
if ch.is_ascii_digit() {
digits.push(ch as u8 - b'0');
}
}
if digits.is_empty() {
return (vec![], 0);
}
let int_len = int_part.chars().filter(|c| c.is_ascii_digit()).count() as i64;
let leading_zeros = digits.iter().take_while(|&&d| d == 0).count();
digits.drain(..leading_zeros);
while digits.last() == Some(&0) {
digits.pop();
}
if digits.is_empty() {
return (vec![], 0);
}
let exponent = (int_len - leading_zeros as i64).saturating_add(exp_part);
let exponent = exponent.clamp(i32::MIN.into(), i32::MAX.into()) as i32;
(digits, exponent)
}
fn split_exponent(s: &str) -> (&str, i64) {
let Some(pos) = s.find(['e', 'E']) else {
return (s, 0);
};
let exponent = match s[pos + 1..].parse::<i64>() {
Ok(exponent) => exponent,
Err(e) => match e.kind() {
std::num::IntErrorKind::PosOverflow => i64::MAX,
std::num::IntErrorKind::NegOverflow => i64::MIN,
_ => 0,
},
};
(&s[..pos], exponent)
}
fn mantissa_to_string(digits: &[u8], width: usize) -> String {
let mut s = String::with_capacity(width);
for &d in digits.iter().take(width) {
s.push((b'0' + d) as char);
}
while s.len() < width {
s.push('0');
}
s
}
fn complement_mantissa(digits: &[u8], width: usize) -> String {
let mut s = String::with_capacity(width);
for i in 0..width {
let d = if i < digits.len() { digits[i] } else { 0 };
s.push((b'0' + (9 - d)) as char);
}
s
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
pub type Item = HashMap<String, AttributeValue>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SseSpecification {
#[serde(rename = "Enabled", default)]
pub enabled: Option<bool>,
#[serde(rename = "SSEType", default)]
pub sse_type: Option<String>,
#[serde(rename = "KMSMasterKeyId", default)]
pub kms_master_key_id: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Tag {
#[serde(rename = "Key")]
pub key: String,
#[serde(rename = "Value")]
pub value: String,
}
pub fn item_size(item: &Item) -> usize {
item.iter()
.map(|(name, value)| name.len() + value.size())
.sum()
}
pub(crate) fn vector_billed_bytes(dimensions: u32) -> usize {
4 * dimensions as usize
}
pub(crate) fn billed_item_size(item: &Item, vector_indexes: &[VectorIndex]) -> usize {
if vector_indexes.is_empty() {
return item_size(item);
}
item.iter()
.map(|(name, value)| {
let billed = vector_indexes
.iter()
.filter(|vix| vix.vector_attribute.attribute_name == *name)
.find(|vix| vector_f32_values(value, vix.dimensions).is_some())
.map(|vix| vector_billed_bytes(vix.dimensions));
name.len() + billed.unwrap_or_else(|| value.size())
})
.sum()
}
pub const MAX_ITEM_SIZE: usize = 400 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemCollectionMetrics {
#[serde(rename = "ItemCollectionKey")]
pub item_collection_key: HashMap<String, AttributeValue>,
#[serde(rename = "SizeEstimateRangeGB")]
pub size_estimate_range_gb: Vec<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConsumedCapacity {
#[serde(rename = "TableName")]
pub table_name: String,
#[serde(rename = "CapacityUnits")]
pub capacity_units: f64,
#[serde(rename = "ReadCapacityUnits", skip_serializing_if = "Option::is_none")]
pub read_capacity_units: Option<f64>,
#[serde(rename = "WriteCapacityUnits", skip_serializing_if = "Option::is_none")]
pub write_capacity_units: Option<f64>,
#[serde(rename = "Table", skip_serializing_if = "Option::is_none")]
pub table: Option<CapacityDetail>,
#[serde(
rename = "GlobalSecondaryIndexes",
skip_serializing_if = "Option::is_none"
)]
pub global_secondary_indexes: Option<HashMap<String, CapacityDetail>>,
#[serde(
rename = "LocalSecondaryIndexes",
skip_serializing_if = "Option::is_none"
)]
pub local_secondary_indexes: Option<HashMap<String, CapacityDetail>>,
#[serde(rename = "VectorIndexes", skip_serializing_if = "Option::is_none")]
pub vector_indexes: Option<HashMap<String, VectorCapacityDetail>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapacityDetail {
#[serde(rename = "CapacityUnits")]
pub capacity_units: f64,
#[serde(rename = "ReadCapacityUnits", skip_serializing_if = "Option::is_none")]
pub read_capacity_units: Option<f64>,
#[serde(rename = "WriteCapacityUnits", skip_serializing_if = "Option::is_none")]
pub write_capacity_units: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VectorCapacityDetail {
#[serde(rename = "VectorWriteRequestBytes")]
pub vector_write_request_bytes: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VectorSearchCapacity {
#[serde(rename = "VectorSearchRequestBytes")]
pub vector_search_request_bytes: f64,
}
pub const MIN_VECTOR_REQUEST_BYTES: f64 = 1024.0;
pub fn vector_request_bytes(bytes: usize) -> f64 {
(bytes as f64).max(MIN_VECTOR_REQUEST_BYTES)
}
pub const TRANSACTIONAL_CAPACITY_FACTOR: f64 = 2.0;
pub fn write_capacity_units(item_size_bytes: usize) -> f64 {
((item_size_bytes as f64) / 1024.0).ceil().max(1.0)
}
pub fn table_write_capacity_units(old_size: Option<usize>, new_size: Option<usize>) -> f64 {
let old = old_size.map(write_capacity_units);
let new = new_size.map(write_capacity_units);
match (old, new) {
(Some(old), Some(new)) => old.max(new),
(Some(units), None) | (None, Some(units)) => units,
(None, None) => write_capacity_units(0),
}
}
pub fn table_read_capacity_units(old_size: Option<usize>, new_size: Option<usize>) -> f64 {
let larger = old_size.unwrap_or(0).max(new_size.unwrap_or(0));
read_capacity_units(larger)
}
pub fn read_capacity_units(item_size_bytes: usize) -> f64 {
((item_size_bytes as f64) / 4096.0).ceil().max(1.0)
}
pub fn read_capacity_units_with_consistency(item_size_bytes: usize, consistent: bool) -> f64 {
let strongly = read_capacity_units(item_size_bytes);
if consistent { strongly } else { strongly / 2.0 }
}
pub fn capacity_wanted(mode: Option<&str>) -> bool {
matches!(mode, Some("TOTAL") | Some("INDEXES"))
}
pub fn vector_capacity_wanted(mode: Option<&str>) -> bool {
matches!(mode, Some("INDEXES"))
}
pub fn consumed_capacity(
table_name: &str,
capacity_units: f64,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
let mode = mode.as_deref().unwrap_or("NONE");
match mode {
"TOTAL" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units,
table: None,
global_secondary_indexes: None,
local_secondary_indexes: None,
..Default::default()
}),
"INDEXES" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units,
table: Some(CapacityDetail {
capacity_units,
..Default::default()
}),
global_secondary_indexes: None,
local_secondary_indexes: None,
..Default::default()
}),
_ => None,
}
}
pub fn consumed_capacity_with_indexes(
table_name: &str,
table_units: f64,
gsi_units: &HashMap<String, f64>,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
consumed_capacity_with_secondary_indexes(
table_name,
table_units,
gsi_units,
&HashMap::new(),
mode,
)
}
pub fn consumed_capacity_with_secondary_indexes(
table_name: &str,
table_units: f64,
gsi_units: &HashMap<String, f64>,
lsi_units: &HashMap<String, f64>,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
secondary_index_capacity(
table_name,
table_units,
gsi_units,
lsi_units,
mode,
CapacityAxis::None,
)
}
#[derive(Clone, Copy, PartialEq)]
enum CapacityAxis {
None,
Write,
}
impl CapacityAxis {
fn write_units(self, units: f64) -> Option<f64> {
match self {
Self::None => None,
Self::Write => Some(units),
}
}
}
fn secondary_index_capacity(
table_name: &str,
table_units: f64,
gsi_units: &HashMap<String, f64>,
lsi_units: &HashMap<String, f64>,
mode: &Option<String>,
axis: CapacityAxis,
) -> Option<ConsumedCapacity> {
let total = table_units + gsi_units.values().sum::<f64>() + lsi_units.values().sum::<f64>();
match mode.as_deref().unwrap_or("NONE") {
"INDEXES" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units: total,
write_capacity_units: axis.write_units(total),
table: Some(CapacityDetail {
capacity_units: table_units,
write_capacity_units: axis.write_units(table_units),
..Default::default()
}),
global_secondary_indexes: capacity_detail_map(gsi_units, axis),
local_secondary_indexes: capacity_detail_map(lsi_units, axis),
..Default::default()
}),
"TOTAL" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units: total,
write_capacity_units: axis.write_units(total),
..Default::default()
}),
_ => None,
}
}
pub fn consumed_capacity_with_vector_indexes(
table_name: &str,
table_units: f64,
gsi_units: &HashMap<String, f64>,
lsi_units: &HashMap<String, f64>,
vector_bytes: &HashMap<String, f64>,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
attach_vector_index_capacity(
consumed_capacity_with_secondary_indexes(
table_name,
table_units,
gsi_units,
lsi_units,
mode,
),
vector_bytes,
mode,
)
}
pub(crate) fn attach_vector_index_capacity(
capacity: Option<ConsumedCapacity>,
vector_bytes: &HashMap<String, f64>,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
let mut capacity = capacity?;
if vector_bytes.is_empty() || !vector_capacity_wanted(mode.as_deref()) {
return Some(capacity);
}
capacity.vector_indexes = Some(
vector_bytes
.iter()
.map(|(name, bytes)| {
(
name.clone(),
VectorCapacityDetail {
vector_write_request_bytes: *bytes,
},
)
})
.collect(),
);
Some(capacity)
}
pub fn transactional_read_capacity(
table_name: &str,
units: f64,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
match mode.as_deref().unwrap_or("NONE") {
"TOTAL" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units: units,
read_capacity_units: Some(units),
..Default::default()
}),
"INDEXES" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units: units,
read_capacity_units: Some(units),
table: Some(CapacityDetail {
capacity_units: units,
read_capacity_units: Some(units),
..Default::default()
}),
..Default::default()
}),
_ => None,
}
}
pub fn build_transactional_capacity(
table_units: &HashMap<String, f64>,
mode: &Option<String>,
builder: fn(&str, f64, &Option<String>) -> Option<ConsumedCapacity>,
) -> Option<Vec<ConsumedCapacity>> {
if !capacity_wanted(mode.as_deref()) {
return None;
}
let mut tables: Vec<&String> = table_units.keys().collect();
tables.sort();
Some(
tables
.into_iter()
.filter_map(|table| builder(table, *table_units.get(table)?, mode))
.collect(),
)
}
pub fn transactional_write_capacity_with_indexes(
table_name: &str,
table_units: f64,
gsi_units: &HashMap<String, f64>,
lsi_units: &HashMap<String, f64>,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
secondary_index_capacity(
table_name,
table_units,
gsi_units,
lsi_units,
mode,
CapacityAxis::Write,
)
}
fn capacity_detail_map(
units: &HashMap<String, f64>,
axis: CapacityAxis,
) -> Option<HashMap<String, CapacityDetail>> {
if units.is_empty() {
return None;
}
Some(
units
.iter()
.map(|(name, &u)| {
(
name.clone(),
CapacityDetail {
capacity_units: u,
write_capacity_units: axis.write_units(u),
..Default::default()
},
)
})
.collect(),
)
}
pub fn transactional_write_capacity(
table_name: &str,
units: f64,
mode: &Option<String>,
) -> Option<ConsumedCapacity> {
match mode.as_deref().unwrap_or("NONE") {
"TOTAL" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units: units,
write_capacity_units: Some(units),
..Default::default()
}),
"INDEXES" => Some(ConsumedCapacity {
table_name: table_name.to_string(),
capacity_units: units,
write_capacity_units: Some(units),
table: Some(CapacityDetail {
capacity_units: units,
write_capacity_units: Some(units),
..Default::default()
}),
..Default::default()
}),
_ => None,
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct KeySchemaElement {
#[serde(rename = "AttributeName", alias = "attribute_name")]
pub attribute_name: String,
#[serde(rename = "KeyType", alias = "key_type")]
pub key_type: KeyType,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum KeyType {
#[default]
HASH,
RANGE,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct AttributeDefinition {
#[serde(rename = "AttributeName", alias = "attribute_name")]
pub attribute_name: String,
#[serde(rename = "AttributeType", alias = "attribute_type")]
pub attribute_type: ScalarAttributeType,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum ScalarAttributeType {
#[default]
S,
N,
B,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Projection {
#[serde(
rename = "ProjectionType",
alias = "projection_type",
default,
skip_serializing_if = "Option::is_none"
)]
pub projection_type: Option<ProjectionType>,
#[serde(
rename = "NonKeyAttributes",
alias = "non_key_attributes",
skip_serializing_if = "Option::is_none"
)]
pub non_key_attributes: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[allow(non_camel_case_types)]
pub enum ProjectionType {
#[default]
ALL,
KEYS_ONLY,
INCLUDE,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct GlobalSecondaryIndex {
#[serde(rename = "IndexName", alias = "index_name")]
pub index_name: String,
#[serde(rename = "KeySchema", alias = "key_schema")]
pub key_schema: Vec<KeySchemaElement>,
#[serde(rename = "Projection", alias = "projection")]
pub projection: Projection,
#[serde(
rename = "ProvisionedThroughput",
alias = "provisioned_throughput",
skip_serializing_if = "Option::is_none"
)]
pub provisioned_throughput: Option<ProvisionedThroughput>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct LocalSecondaryIndex {
#[serde(rename = "IndexName", alias = "index_name")]
pub index_name: String,
#[serde(rename = "KeySchema", alias = "key_schema")]
pub key_schema: Vec<KeySchemaElement>,
#[serde(rename = "Projection", alias = "projection")]
pub projection: Projection,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct VectorAttributeDefinition {
#[serde(rename = "AttributeName", alias = "attribute_name")]
pub attribute_name: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SearchSchemaElement {
#[serde(rename = "AttributeName", alias = "attribute_name")]
pub attribute_name: String,
#[serde(
rename = "SearchSchemaElementType",
alias = "search_schema_element_type"
)]
pub search_schema_element_type: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct VectorIndex {
#[serde(rename = "IndexName", alias = "index_name")]
pub index_name: String,
#[serde(rename = "VectorAttribute", alias = "vector_attribute")]
pub vector_attribute: VectorAttributeDefinition,
#[serde(
rename = "SearchSchema",
alias = "search_schema",
default,
skip_serializing_if = "Option::is_none"
)]
pub search_schema: Option<Vec<SearchSchemaElement>>,
#[serde(rename = "Projection", alias = "projection")]
pub projection: Projection,
#[serde(rename = "Dimensions", alias = "dimensions")]
pub dimensions: u32,
#[serde(rename = "DistanceFunction", alias = "distance_function")]
pub distance_function: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum VectorValueError {
NotAList,
WrongDimensions {
actual: usize,
},
ElementNotANumber {
position: usize,
actual: &'static str,
},
ElementOutOfRange {
position: usize,
value: String,
},
}
pub fn check_vector_f32_values(
value: &AttributeValue,
dimensions: u32,
) -> std::result::Result<Vec<f32>, VectorValueError> {
let AttributeValue::L(elems) = value else {
return Err(VectorValueError::NotAList);
};
if elems.len() != dimensions as usize {
return Err(VectorValueError::WrongDimensions {
actual: elems.len(),
});
}
let mut out = Vec::with_capacity(elems.len());
for (position, elem) in elems.iter().enumerate() {
let AttributeValue::N(n) = elem else {
return Err(VectorValueError::ElementNotANumber {
position,
actual: elem.type_name(),
});
};
match n.parse::<f32>() {
Ok(v) if v.is_finite() => out.push(v),
_ => {
return Err(VectorValueError::ElementOutOfRange {
position,
value: n.clone(),
});
}
}
}
Ok(out)
}
pub fn vector_f32_values(value: &AttributeValue, dimensions: u32) -> Option<Vec<f32>> {
check_vector_f32_values(value, dimensions).ok()
}
pub fn f32_number_string(v: f32) -> String {
serde_json::to_string(&v).unwrap_or_else(|_| "0.0".to_string())
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ProvisionedThroughput {
#[serde(rename = "ReadCapacityUnits", alias = "read_capacity_units", default)]
pub read_capacity_units: Option<i64>,
#[serde(rename = "WriteCapacityUnits", alias = "write_capacity_units", default)]
pub write_capacity_units: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct OnDemandThroughput {
#[serde(
rename = "MaxReadRequestUnits",
alias = "max_read_request_units",
default,
skip_serializing_if = "Option::is_none"
)]
pub max_read_request_units: Option<i64>,
#[serde(
rename = "MaxWriteRequestUnits",
alias = "max_write_request_units",
default,
skip_serializing_if = "Option::is_none"
)]
pub max_write_request_units: Option<i64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConversionError {
pub expected: &'static str,
pub actual: &'static str,
}
impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "expected {}, got {}", self.expected, self.actual)
}
}
impl std::error::Error for ConversionError {}
impl From<String> for AttributeValue {
fn from(value: String) -> Self {
AttributeValue::S(value)
}
}
impl From<&str> for AttributeValue {
fn from(value: &str) -> Self {
AttributeValue::S(value.to_string())
}
}
impl From<bool> for AttributeValue {
fn from(value: bool) -> Self {
AttributeValue::BOOL(value)
}
}
impl From<Vec<u8>> for AttributeValue {
fn from(value: Vec<u8>) -> Self {
AttributeValue::B(value)
}
}
impl From<&[u8]> for AttributeValue {
fn from(value: &[u8]) -> Self {
AttributeValue::B(value.to_vec())
}
}
macro_rules! impl_from_integer {
($($t:ty),+) => {
$(
impl From<$t> for AttributeValue {
fn from(value: $t) -> Self {
AttributeValue::N(value.to_string())
}
}
)+
};
}
impl_from_integer!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
impl From<HashMap<String, AttributeValue>> for AttributeValue {
fn from(value: HashMap<String, AttributeValue>) -> Self {
AttributeValue::M(value)
}
}
impl From<Vec<AttributeValue>> for AttributeValue {
fn from(value: Vec<AttributeValue>) -> Self {
AttributeValue::L(value)
}
}
impl From<HashSet<String>> for AttributeValue {
fn from(value: HashSet<String>) -> Self {
AttributeValue::SS(value.into_iter().collect())
}
}
impl From<BTreeSet<String>> for AttributeValue {
fn from(value: BTreeSet<String>) -> Self {
AttributeValue::SS(value.into_iter().collect())
}
}
impl TryFrom<f64> for AttributeValue {
type Error = ConversionError;
fn try_from(value: f64) -> std::result::Result<Self, Self::Error> {
if value.is_finite() {
Ok(AttributeValue::N(value.to_string()))
} else {
Err(ConversionError {
expected: "finite f64",
actual: "NaN or Infinity",
})
}
}
}
impl TryFrom<f32> for AttributeValue {
type Error = ConversionError;
fn try_from(value: f32) -> std::result::Result<Self, Self::Error> {
if value.is_finite() {
Ok(AttributeValue::N(value.to_string()))
} else {
Err(ConversionError {
expected: "finite f32",
actual: "NaN or Infinity",
})
}
}
}
impl TryFrom<AttributeValue> for String {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::S(s) => Ok(s),
other => Err(ConversionError {
expected: "S",
actual: other.type_name(),
}),
}
}
}
impl TryFrom<AttributeValue> for bool {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::BOOL(b) => Ok(b),
other => Err(ConversionError {
expected: "BOOL",
actual: other.type_name(),
}),
}
}
}
impl TryFrom<AttributeValue> for Vec<u8> {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::B(b) => Ok(b),
other => Err(ConversionError {
expected: "B",
actual: other.type_name(),
}),
}
}
}
macro_rules! impl_try_from_av_integer {
($($t:ty),+) => {
$(
impl TryFrom<AttributeValue> for $t {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::N(n) => n.parse::<$t>().map_err(|_| ConversionError {
expected: stringify!($t),
actual: "N (parse failed)",
}),
other => Err(ConversionError {
expected: "N",
actual: other.type_name(),
}),
}
}
}
)+
};
}
impl_try_from_av_integer!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
impl TryFrom<AttributeValue> for f64 {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::N(n) => n.parse::<f64>().map_err(|_| ConversionError {
expected: "f64",
actual: "N (parse failed)",
}),
other => Err(ConversionError {
expected: "N",
actual: other.type_name(),
}),
}
}
}
impl TryFrom<AttributeValue> for f32 {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::N(n) => n.parse::<f32>().map_err(|_| ConversionError {
expected: "f32",
actual: "N (parse failed)",
}),
other => Err(ConversionError {
expected: "N",
actual: other.type_name(),
}),
}
}
}
impl TryFrom<AttributeValue> for HashMap<String, AttributeValue> {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::M(m) => Ok(m),
other => Err(ConversionError {
expected: "M",
actual: other.type_name(),
}),
}
}
}
impl TryFrom<AttributeValue> for Vec<AttributeValue> {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::L(l) => Ok(l),
other => Err(ConversionError {
expected: "L",
actual: other.type_name(),
}),
}
}
}
impl TryFrom<AttributeValue> for Vec<String> {
type Error = ConversionError;
fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
match value {
AttributeValue::SS(ss) => Ok(ss),
AttributeValue::L(l) => {
l.into_iter()
.map(|av| match av {
AttributeValue::S(s) => Ok(s),
other => Err(ConversionError {
expected: "S (within L)",
actual: other.type_name(),
}),
})
.collect()
}
other => Err(ConversionError {
expected: "SS or L",
actual: other.type_name(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_string() {
let val = AttributeValue::S("hello".to_string());
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"S":"hello"}"#);
}
#[test]
fn test_serialize_number() {
let val = AttributeValue::N("42".to_string());
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"N":"42"}"#);
}
#[test]
fn test_serialize_binary() {
let val = AttributeValue::B(vec![1, 2, 3]);
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"B":"AQID"}"#);
}
#[test]
fn test_serialize_bool() {
let val = AttributeValue::BOOL(true);
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"BOOL":true}"#);
}
#[test]
fn test_serialize_null() {
let val = AttributeValue::NULL(true);
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"NULL":true}"#);
}
#[test]
fn test_deserialize_null_true() {
let val: AttributeValue = serde_json::from_str(r#"{"NULL":true}"#).unwrap();
assert_eq!(val, AttributeValue::NULL(true));
}
#[test]
fn test_deserialize_null_false_rejected() {
let err = serde_json::from_str::<AttributeValue>(r#"{"NULL":false}"#).unwrap_err();
assert!(
err.to_string().contains("must have the value of true"),
"unexpected error: {err}"
);
}
#[test]
fn test_deserialize_null_non_boolean_rejected() {
let err = serde_json::from_str::<AttributeValue>(r#"{"NULL":"no"}"#).unwrap_err();
assert!(
err.to_string().contains("must have the value of true"),
"unexpected error: {err}"
);
}
#[test]
fn test_serialize_string_set() {
let val = AttributeValue::SS(vec!["a".to_string(), "b".to_string()]);
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"SS":["a","b"]}"#);
}
#[test]
fn test_serialize_list() {
let val = AttributeValue::L(vec![
AttributeValue::S("hello".to_string()),
AttributeValue::N("42".to_string()),
]);
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"L":[{"S":"hello"},{"N":"42"}]}"#);
}
#[test]
fn test_serialize_map() {
let mut m = HashMap::new();
m.insert("key".to_string(), AttributeValue::S("value".to_string()));
let val = AttributeValue::M(m);
let json = serde_json::to_string(&val).unwrap();
assert_eq!(json, r#"{"M":{"key":{"S":"value"}}}"#);
}
#[test]
fn test_round_trip_all_types() {
let values = vec![
AttributeValue::S("hello".to_string()),
AttributeValue::N("42.5".to_string()),
AttributeValue::B(vec![0, 255, 128]),
AttributeValue::BOOL(false),
AttributeValue::NULL(true),
AttributeValue::SS(vec!["x".to_string(), "y".to_string()]),
AttributeValue::NS(vec!["1".to_string(), "2.5".to_string()]),
AttributeValue::BS(vec![vec![1], vec![2, 3]]),
AttributeValue::L(vec![
AttributeValue::S("nested".to_string()),
AttributeValue::N("99".to_string()),
]),
];
for val in values {
let json = serde_json::to_string(&val).unwrap();
let deserialized: AttributeValue = serde_json::from_str(&json).unwrap();
assert_eq!(val, deserialized, "Round-trip failed for {json}");
}
}
#[test]
fn test_size_string() {
let val = AttributeValue::S("hello".to_string());
assert_eq!(val.size(), 5);
}
#[test]
fn test_size_number() {
let val = AttributeValue::N("42".to_string());
assert_eq!(val.size(), 2);
}
#[test]
fn test_size_number_matches_dynamodb() {
let cases: &[(&str, usize)] = &[
("1", 2),
("12", 2),
("123", 3),
("1234", 3),
("12345", 4),
("123456", 4),
("1234567", 5),
("12345678", 5),
("123456789", 6),
("1234567891", 6),
("12345678912", 7),
("123456789123", 7),
("1234567890123456789", 11),
("12345678901234567890123456789012345678", 20),
("0042", 2),
("100", 2),
("1010", 3),
("0.0000001", 2),
("0", 1),
("1E125", 2),
("1E-100", 2),
("1.5", 3),
("1.2", 3),
("1.200", 3),
("1.234", 4),
("3.14159", 5),
("100.5", 4),
("0.15", 2),
("15", 2),
("-42", 3),
("-12345", 5),
("-1E125", 3),
("-0.15", 3),
("-0", 1),
];
for (literal, expected) in cases {
assert_eq!(
AttributeValue::N((*literal).to_string()).size(),
*expected,
"size of N {literal:?}"
);
}
}
#[test]
fn test_size_number_survives_normalisation() {
for literal in [
"1E125",
"1E-100",
"0042",
"1.200",
"0.0000001",
"100",
"3.14159",
"42",
] {
let raw = AttributeValue::N(literal.to_string()).size();
let normalised = AttributeValue::N(normalize_dynamo_number(literal)).size();
assert_eq!(
raw, normalised,
"N {literal:?} changed size when normalised"
);
}
}
#[test]
fn test_size_bool() {
assert_eq!(AttributeValue::BOOL(true).size(), 1);
}
#[test]
fn test_size_null() {
assert_eq!(AttributeValue::NULL(true).size(), 1);
}
#[test]
fn test_key_string_s() {
let val = AttributeValue::S("hello".to_string());
assert_eq!(val.to_key_string(), Some("S:hello".to_string()));
}
#[test]
fn test_key_string_n() {
let val = AttributeValue::N("42".to_string());
let key = val.to_key_string().unwrap();
assert!(key.starts_with("N:"));
}
#[test]
fn test_key_string_b() {
let val = AttributeValue::B(vec![0xff, 0x00, 0xab]);
assert_eq!(val.to_key_string(), Some("B:ff00ab".to_string()));
}
#[test]
fn test_key_string_non_key_type_returns_none() {
assert_eq!(AttributeValue::BOOL(true).to_key_string(), None);
assert_eq!(AttributeValue::L(vec![]).to_key_string(), None);
}
#[test]
fn test_number_sort_ordering() {
let numbers = vec![
"-1000", "-100", "-10", "-1", "-0.5", "-0.001", "0", "0.001", "0.5", "1", "10", "100",
"1000",
];
let encoded: Vec<String> = numbers
.iter()
.map(|n| normalize_number_for_sort(n))
.collect();
for i in 0..encoded.len() - 1 {
assert!(
encoded[i] < encoded[i + 1],
"Sort order broken: {} ({}) should be < {} ({})",
numbers[i],
encoded[i],
numbers[i + 1],
encoded[i + 1]
);
}
}
#[test]
fn test_number_sort_zero_variants() {
let z1 = normalize_number_for_sort("0");
let z2 = normalize_number_for_sort("-0");
let z3 = normalize_number_for_sort("0.0");
assert_eq!(z1, z2);
assert_eq!(z2, z3);
}
#[test]
fn test_number_sort_decimals() {
let a = normalize_number_for_sort("1.5");
let b = normalize_number_for_sort("2.5");
assert!(a < b);
let c = normalize_number_for_sort("0.001");
let d = normalize_number_for_sort("0.01");
assert!(c < d);
}
#[test]
fn test_number_sort_scientific() {
let a = normalize_number_for_sort("1e10");
let b = normalize_number_for_sort("1e11");
assert!(a < b);
let c = normalize_number_for_sort("-1e11");
let d = normalize_number_for_sort("-1e10");
assert!(c < d);
}
#[test]
fn test_number_sort_out_of_range_sorts_at_the_extremes() {
let ordered = [
"-1e99999999999",
"-9.9999999999999999999999999999999999999E125",
"-1E-130",
"-1e-99999999999",
"0",
"1e-99999999999",
"1E-130",
"9.9999999999999999999999999999999999999E125",
"1e99999999999",
];
for pair in ordered.windows(2) {
let (a, b) = (pair[0], pair[1]);
assert!(
normalize_number_for_sort(a) < normalize_number_for_sort(b),
"{a} should sort before {b}"
);
}
}
#[test]
fn test_split_exponent_reads_an_unparseable_exponent_as_zero() {
assert_eq!(split_exponent("1e"), ("1", 0));
assert_eq!(split_exponent("1e+"), ("1", 0));
assert_eq!(split_exponent("1e99999999999999999999"), ("1", i64::MAX));
assert_eq!(split_exponent("1e-99999999999999999999"), ("1", i64::MIN));
}
#[test]
fn test_validate_number_accepts_dynamodb_grammar() {
for input in [
"+5", "+1.5", "+0", "-0", "+0.0", "+1e2", "1e+2", "1.5E+3", "-7", "+.5", ".5", "5.",
"00042", "1.23E10", "+1e-2", "1E-130", "+1E-130",
] {
assert!(
validate_dynamo_number(input).is_ok(),
"expected {input} to validate, got {:?}",
validate_dynamo_number(input)
);
}
}
#[test]
fn test_validate_number_rejects_malformed() {
for input in [
"+e2", "e2", "+1+2", "1+2", "+1.2.3", "1.2.3", "++5", "+-5", "-+5", "+", "-", "1e",
"1e+", ".", "1.2e3.4", "0x5", "NaN", "Infinity", "1_000", " 5", "5 ", "1 5", "",
] {
assert!(
matches!(
validate_dynamo_number(input),
Err(crate::errors::DynoxideError::ValidationException(_))
),
"expected {input:?} to be rejected with ValidationException, got {:?}",
validate_dynamo_number(input)
);
}
}
fn validation_message(input: &str) -> Option<String> {
match validate_dynamo_number(input) {
Ok(()) => None,
Err(crate::errors::DynoxideError::ValidationException(m)) => Some(m),
Err(e) => panic!("expected a ValidationException for {input:?}, got {e:?}"),
}
}
const OVERFLOW: &str =
"Number overflow. Attempting to store a number with magnitude larger than supported range";
const UNDERFLOW: &str = "Number underflow. Attempting to store a number with magnitude smaller than supported range";
#[test]
fn test_validate_number_range_checks_exponents_beyond_i32() {
for (input, expected) in [
("1e99999999999", OVERFLOW),
("-1e99999999999", OVERFLOW),
("1E+99999999999", OVERFLOW),
("1e-99999999999", UNDERFLOW),
("-1e-99999999999", UNDERFLOW),
("1e2147483648", OVERFLOW),
("1e-2147483649", UNDERFLOW),
("10e2147483647", OVERFLOW),
("0.01e-2147483648", UNDERFLOW),
("1e9223372036854775807", OVERFLOW),
("1e-9223372036854775808", UNDERFLOW),
("10e9223372036854775807", OVERFLOW),
("0.01e-9223372036854775808", UNDERFLOW),
] {
assert_eq!(
validation_message(input).as_deref(),
Some(expected),
"{input}"
);
}
}
#[test]
fn test_validate_number_rejects_exponent_beyond_i64_as_unconvertible() {
for input in [
"1e9223372036854775808",
"1e+9223372036854775808",
"1e-9223372036854775809",
"0e9223372036854775808",
] {
assert_eq!(
validation_message(input),
Some(format!(
"The parameter cannot be converted to a numeric value: {input}"
)),
"{input}"
);
}
}
#[test]
fn test_validate_number_range_checks_zero_by_its_last_written_digit() {
let zeros_130 = format!("0.{}", "0".repeat(130));
let zeros_131 = format!("0.{}", "0".repeat(131));
for (input, expected) in [
("0e125", None),
("0e126", Some(OVERFLOW)),
("0.0e126", None),
("0.0e127", Some(OVERFLOW)),
("0.00e127", None),
("00e126", Some(OVERFLOW)),
("00.00e126", None),
("-0e127", Some(OVERFLOW)),
("0e99999999999", Some(OVERFLOW)),
("0e-130", None),
("0e-131", Some(UNDERFLOW)),
("0.0e-129", None),
("0.0e-130", Some(UNDERFLOW)),
("0e-99999999999", Some(UNDERFLOW)),
(zeros_130.as_str(), None),
(zeros_131.as_str(), Some(UNDERFLOW)),
] {
assert_eq!(validation_message(input).as_deref(), expected, "{input}");
}
}
#[test]
fn test_normalize_number_matches_dynamodb() {
for (input, stored) in [
("+5", "5"),
("+1.5", "1.5"),
("+0", "0"),
("-0", "0"),
("+0.0", "0"),
("+1e2", "100"),
("1e+2", "100"),
("1.5E+3", "1500"),
("-7", "-7"),
("+.5", "0.5"),
(".5", "0.5"),
("5.", "5"),
("00042", "42"),
("1.23E10", "12300000000"),
("+1e-2", "0.01"),
] {
assert_eq!(
normalize_dynamo_number(input),
stored,
"{input} should normalise to {stored}"
);
}
}
#[test]
fn test_type_name() {
assert_eq!(AttributeValue::S("".to_string()).type_name(), "S");
assert_eq!(AttributeValue::N("0".to_string()).type_name(), "N");
assert_eq!(AttributeValue::B(vec![]).type_name(), "B");
assert_eq!(AttributeValue::BOOL(true).type_name(), "BOOL");
assert_eq!(AttributeValue::NULL(true).type_name(), "NULL");
assert_eq!(AttributeValue::SS(vec![]).type_name(), "SS");
assert_eq!(AttributeValue::NS(vec![]).type_name(), "NS");
assert_eq!(AttributeValue::BS(vec![]).type_name(), "BS");
assert_eq!(AttributeValue::L(vec![]).type_name(), "L");
assert_eq!(AttributeValue::M(HashMap::new()).type_name(), "M");
}
}