Skip to main content

dynoxide/
types.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
2use serde::de;
3use serde::ser::SerializeMap;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::collections::{BTreeSet, HashMap, HashSet};
6use std::fmt;
7
8/// DynamoDB AttributeValue — the core type system.
9///
10/// Each variant corresponds to a DynamoDB type descriptor:
11/// S (String), N (Number as string), B (Binary), BOOL, NULL,
12/// SS (String Set), NS (Number Set), BS (Binary Set),
13/// L (List), M (Map).
14#[derive(Debug, Clone, PartialEq)]
15pub enum AttributeValue {
16    /// String type
17    S(String),
18    /// Number type — stored as string per DynamoDB convention
19    N(String),
20    /// Binary type — raw bytes, serialized as base64
21    B(Vec<u8>),
22    /// Boolean type
23    BOOL(bool),
24    /// Null type
25    NULL(bool),
26    /// String Set
27    SS(Vec<String>),
28    /// Number Set — each number stored as string
29    NS(Vec<String>),
30    /// Binary Set — each element is raw bytes
31    BS(Vec<Vec<u8>>),
32    /// List — ordered collection of AttributeValues
33    L(Vec<AttributeValue>),
34    /// Map — key-value pairs
35    M(HashMap<String, AttributeValue>),
36}
37
38impl AttributeValue {
39    /// Calculate the size of this attribute value in bytes,
40    /// following DynamoDB's item size calculation rules.
41    ///
42    /// This does NOT include the attribute name — the caller
43    /// is responsible for adding the name's UTF-8 byte length.
44    pub fn size(&self) -> usize {
45        match self {
46            AttributeValue::S(s) => s.len(),
47            AttributeValue::N(n) => {
48                // DynamoDB: (number of significant digits / 2) + 1, minimum 1
49                let significant = n.chars().filter(|c| c.is_ascii_digit()).count();
50                let significant = significant.max(1);
51                (significant / 2) + 1
52            }
53            AttributeValue::B(b) => b.len(),
54            AttributeValue::BOOL(_) => 1,
55            AttributeValue::NULL(_) => 1,
56            AttributeValue::SS(ss) => ss.iter().map(|s| s.len()).sum(),
57            AttributeValue::NS(ns) => ns
58                .iter()
59                .map(|n| {
60                    let significant = n.chars().filter(|c| c.is_ascii_digit()).count().max(1);
61                    (significant / 2) + 1
62                })
63                .sum(),
64            AttributeValue::BS(bs) => bs.iter().map(|b| b.len()).sum(),
65            AttributeValue::L(items) => {
66                // List overhead: 3 bytes + 1 byte per element + sum of element sizes
67                3 + items.len() + items.iter().map(|v| v.size()).sum::<usize>()
68            }
69            AttributeValue::M(map) => {
70                // Map overhead: 3 bytes + sum of (key_len + 1 + value_size) per entry
71                3 + map
72                    .iter()
73                    .map(|(k, v)| k.len() + 1 + v.size())
74                    .sum::<usize>()
75            }
76        }
77    }
78
79    /// Returns the DynamoDB type descriptor string for this value.
80    pub fn type_name(&self) -> &'static str {
81        match self {
82            AttributeValue::S(_) => "S",
83            AttributeValue::N(_) => "N",
84            AttributeValue::B(_) => "B",
85            AttributeValue::BOOL(_) => "BOOL",
86            AttributeValue::NULL(_) => "NULL",
87            AttributeValue::SS(_) => "SS",
88            AttributeValue::NS(_) => "NS",
89            AttributeValue::BS(_) => "BS",
90            AttributeValue::L(_) => "L",
91            AttributeValue::M(_) => "M",
92        }
93    }
94
95    /// Returns true if this is a scalar type (S, N, B, BOOL, NULL).
96    pub fn is_scalar(&self) -> bool {
97        matches!(
98            self,
99            AttributeValue::S(_)
100                | AttributeValue::N(_)
101                | AttributeValue::B(_)
102                | AttributeValue::BOOL(_)
103                | AttributeValue::NULL(_)
104        )
105    }
106
107    /// Returns true if this is a set type (SS, NS, BS).
108    pub fn is_set(&self) -> bool {
109        matches!(
110            self,
111            AttributeValue::SS(_) | AttributeValue::NS(_) | AttributeValue::BS(_)
112        )
113    }
114
115    /// Serialize this value to a deterministic TEXT representation
116    /// for use as a SQLite primary key column (pk or sk).
117    ///
118    /// - S: stored as-is (UTF-8 text sorts correctly)
119    /// - N: normalized to a comparable string encoding
120    /// - B: hex-encoded (preserves byte ordering)
121    pub fn to_key_string(&self) -> Option<String> {
122        match self {
123            AttributeValue::S(s) => Some(format!("S:{s}")),
124            AttributeValue::N(n) => Some(format!("N:{}", normalize_number_for_sort(n))),
125            AttributeValue::B(b) => Some(format!("B:{}", hex_encode(b))),
126            _ => None, // Only S, N, B can be key types
127        }
128    }
129}
130
131impl fmt::Display for AttributeValue {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            AttributeValue::S(s) => write!(f, "\"{s}\""),
135            AttributeValue::N(n) => write!(f, "{n}"),
136            AttributeValue::B(b) => write!(f, "<binary {} bytes>", b.len()),
137            AttributeValue::BOOL(b) => write!(f, "{b}"),
138            AttributeValue::NULL(_) => write!(f, "null"),
139            AttributeValue::SS(ss) => write!(f, "{ss:?}"),
140            AttributeValue::NS(ns) => write!(f, "{ns:?}"),
141            AttributeValue::BS(bs) => write!(f, "<binary set {} items>", bs.len()),
142            AttributeValue::L(items) => write!(f, "<list {} items>", items.len()),
143            AttributeValue::M(map) => write!(f, "<map {} keys>", map.len()),
144        }
145    }
146}
147
148// ---------------------------------------------------------------------------
149// Custom serde: DynamoDB JSON format {"S": "hello"}, {"N": "42"}, etc.
150// ---------------------------------------------------------------------------
151
152impl Serialize for AttributeValue {
153    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
154    where
155        S: Serializer,
156    {
157        let mut map = serializer.serialize_map(Some(1))?;
158        match self {
159            AttributeValue::S(s) => map.serialize_entry("S", s)?,
160            AttributeValue::N(n) => map.serialize_entry("N", n)?,
161            AttributeValue::B(b) => {
162                map.serialize_entry("B", &BASE64.encode(b))?;
163            }
164            AttributeValue::BOOL(b) => map.serialize_entry("BOOL", b)?,
165            AttributeValue::NULL(n) => map.serialize_entry("NULL", n)?,
166            AttributeValue::SS(ss) => map.serialize_entry("SS", ss)?,
167            AttributeValue::NS(ns) => map.serialize_entry("NS", ns)?,
168            AttributeValue::BS(bs) => {
169                let encoded: Vec<String> = bs.iter().map(|b| BASE64.encode(b)).collect();
170                map.serialize_entry("BS", &encoded)?;
171            }
172            AttributeValue::L(items) => map.serialize_entry("L", items)?,
173            AttributeValue::M(m) => map.serialize_entry("M", m)?,
174        }
175        map.end()
176    }
177}
178
179impl<'de> Deserialize<'de> for AttributeValue {
180    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
181    where
182        D: Deserializer<'de>,
183    {
184        // Deserialize as raw JSON Value first so we can inspect all keys
185        let raw = serde_json::Value::deserialize(deserializer)?;
186
187        let obj = raw
188            .as_object()
189            .ok_or_else(|| de::Error::custom("empty AttributeValue object"))?;
190
191        if obj.is_empty() {
192            return Err(de::Error::custom("empty AttributeValue object"));
193        }
194
195        // Collect known type keys
196        let known_types = ["S", "N", "B", "BOOL", "NULL", "SS", "NS", "BS", "L", "M"];
197        let present: Vec<&str> = obj
198            .keys()
199            .filter(|k| known_types.contains(&k.as_str()))
200            .map(|k| k.as_str())
201            .collect();
202
203        if present.is_empty() {
204            return Err(de::Error::custom(
205                "Supplied AttributeValue is empty, must contain exactly one of the supported datatypes",
206            ));
207        }
208
209        // Validate numbers in ALL type keys before checking for multi-type.
210        // DynamoDB validates number format before rejecting multi-type.
211        for &type_key in &present {
212            match type_key {
213                "N" => {
214                    if let Some(n) = obj.get("N").and_then(|v| v.as_str()) {
215                        validate_number_in_deser(n).map_err(de::Error::custom)?;
216                    }
217                }
218                "NS" => {
219                    if let Some(arr) = obj.get("NS").and_then(|v| v.as_array()) {
220                        for item in arr {
221                            if let Some(n) = item.as_str() {
222                                validate_number_in_deser(n).map_err(de::Error::custom)?;
223                            }
224                        }
225                    }
226                }
227                _ => {}
228            }
229        }
230
231        // Check for multiple type keys
232        if present.len() > 1 {
233            return Err(de::Error::custom(
234                "VALIDATION:Supplied AttributeValue has more than one datatypes set, \
235                 must contain exactly one of the supported datatypes",
236            ));
237        }
238
239        let type_key = present[0];
240        let val = &obj[type_key];
241
242        match type_key {
243            "S" => {
244                let s = val
245                    .as_str()
246                    .ok_or_else(|| de::Error::custom("expected string for S"))?;
247                Ok(AttributeValue::S(s.to_string()))
248            }
249            "N" => {
250                let n = val
251                    .as_str()
252                    .ok_or_else(|| de::Error::custom("expected string for N"))?;
253                Ok(AttributeValue::N(n.to_string()))
254            }
255            "B" => {
256                let encoded = val
257                    .as_str()
258                    .ok_or_else(|| de::Error::custom("expected string for B"))?;
259                let bytes = BASE64
260                    .decode(encoded)
261                    .map_err(|e| de::Error::custom(format!("invalid base64: {e}")))?;
262                Ok(AttributeValue::B(bytes))
263            }
264            "BOOL" => {
265                let b = val
266                    .as_bool()
267                    .ok_or_else(|| de::Error::custom("expected boolean for BOOL"))?;
268                Ok(AttributeValue::BOOL(b))
269            }
270            "NULL" => {
271                // The NULL member is a plain boolean in the model. AWS accepts both
272                // {"NULL": true} and {"NULL": false} and reads either back as
273                // {"NULL": true}, so normalise false to true here. A non-boolean
274                // value (e.g. {"NULL": "no"}) is a type error.
275                if val.as_bool().is_none() {
276                    return Err(de::Error::custom(
277                        "VALIDATION:One or more parameter values were invalid: \
278                         Null attribute value types must have the value of true",
279                    ));
280                }
281                Ok(AttributeValue::NULL(true))
282            }
283            "SS" => {
284                let arr = val
285                    .as_array()
286                    .ok_or_else(|| de::Error::custom("expected array for SS"))?;
287                let ss: Result<Vec<String>, _> = arr
288                    .iter()
289                    .map(|v| {
290                        v.as_str()
291                            .map(|s| s.to_string())
292                            .ok_or_else(|| de::Error::custom("expected string in SS"))
293                    })
294                    .collect();
295                Ok(AttributeValue::SS(ss?))
296            }
297            "NS" => {
298                let arr = val
299                    .as_array()
300                    .ok_or_else(|| de::Error::custom("expected array for NS"))?;
301                let ns: Result<Vec<String>, _> = arr
302                    .iter()
303                    .map(|v| {
304                        v.as_str()
305                            .map(|s| s.to_string())
306                            .ok_or_else(|| de::Error::custom("expected string in NS"))
307                    })
308                    .collect();
309                Ok(AttributeValue::NS(ns?))
310            }
311            "BS" => {
312                let arr = val
313                    .as_array()
314                    .ok_or_else(|| de::Error::custom("expected array for BS"))?;
315                let mut decoded = Vec::with_capacity(arr.len());
316                for item in arr {
317                    let encoded = item
318                        .as_str()
319                        .ok_or_else(|| de::Error::custom("expected string in BS"))?;
320                    decoded.push(
321                        BASE64
322                            .decode(encoded)
323                            .map_err(|e| de::Error::custom(format!("invalid base64: {e}")))?,
324                    );
325                }
326                Ok(AttributeValue::BS(decoded))
327            }
328            "L" => {
329                let arr = val
330                    .as_array()
331                    .ok_or_else(|| de::Error::custom("expected array for L"))?;
332                let list: Result<Vec<AttributeValue>, _> = arr
333                    .iter()
334                    .map(|v| serde_json::from_value(v.clone()).map_err(de::Error::custom))
335                    .collect();
336                Ok(AttributeValue::L(list?))
337            }
338            "M" => {
339                let map_val = val
340                    .as_object()
341                    .ok_or_else(|| de::Error::custom("expected object for M"))?;
342                let mut result = std::collections::HashMap::new();
343                for (k, v) in map_val {
344                    let av: AttributeValue =
345                        serde_json::from_value(v.clone()).map_err(de::Error::custom)?;
346                    result.insert(k.clone(), av);
347                }
348                Ok(AttributeValue::M(result))
349            }
350            _ => unreachable!(),
351        }
352    }
353}
354
355/// Validate a number string during AttributeValue deserialization.
356///
357/// Returns DynamoDB-matching error messages for invalid numbers.
358/// Error messages are returned WITHOUT the VALIDATION: prefix since they
359/// bypass the normal validation flow — the server routes them based on
360/// message content (see `server::deserialize`).
361fn validate_number_in_deser(n: &str) -> Result<(), String> {
362    if n.is_empty() {
363        return Err("VALIDATION:The parameter cannot be converted to a numeric value".to_string());
364    }
365    // Check if it's a valid number
366    let trimmed = n.trim();
367    let is_valid = trimmed.parse::<f64>().is_ok()
368        || trimmed
369            .to_lowercase()
370            .contains('e')
371            .then(|| trimmed.parse::<f64>().ok())
372            .is_some();
373    if !is_valid {
374        return Err(format!(
375            "VALIDATION:The parameter cannot be converted to a numeric value: {n}"
376        ));
377    }
378    // Use the full validate_dynamo_number for precision/range checks
379    if let Err(e) = validate_dynamo_number(n) {
380        let msg = match e {
381            crate::errors::DynoxideError::ValidationException(m) => format!("VALIDATION:{m}"),
382            _ => format!("VALIDATION:{}", e),
383        };
384        return Err(msg);
385    }
386    Ok(())
387}
388
389// ---------------------------------------------------------------------------
390// Number sort key normalization
391// ---------------------------------------------------------------------------
392
393/// Normalize a DynamoDB number string into a comparable string that sorts
394/// correctly in SQLite TEXT collation.
395///
396/// Encoding scheme:
397/// - Positive numbers: "1" + zero-padded exponent (4 digits, offset by 5000) + normalized mantissa
398/// - Zero: "1" + "5000" + "0" (padded)
399/// - Negative numbers: "0" + complement of (exponent + mantissa) so they sort before positives
400///
401/// DynamoDB numbers: up to 38 digits of precision, range ~-1E+126 to ~+1E+126.
402pub fn normalize_number_for_sort(num_str: &str) -> String {
403    let trimmed = num_str.trim();
404
405    if trimmed.is_empty() || trimmed == "0" || trimmed == "-0" || trimmed == "0.0" {
406        return zero_encoding();
407    }
408
409    let negative = trimmed.starts_with('-');
410    let abs_str = if negative { &trimmed[1..] } else { trimmed };
411
412    // Parse into mantissa digits and exponent
413    let (mantissa_digits, exponent) = parse_number_parts(abs_str);
414
415    if mantissa_digits.is_empty() || mantissa_digits.iter().all(|&d| d == 0) {
416        return zero_encoding();
417    }
418
419    if negative {
420        encode_negative(&mantissa_digits, exponent)
421    } else {
422        encode_positive(&mantissa_digits, exponent)
423    }
424}
425
426/// Validate a DynamoDB number string against DynamoDB's constraints:
427/// - Up to 38 significant digits
428/// - Magnitude at most 9.9999999999999999999999999999999999999E+125
429/// - Positive values must be at least 1E-130
430/// - Negative values must be at most -1E-130
431pub fn validate_dynamo_number(
432    num_str: &str,
433) -> std::result::Result<(), crate::errors::DynoxideError> {
434    let trimmed = num_str.trim();
435
436    if trimmed.is_empty() {
437        return Err(crate::errors::DynoxideError::ValidationException(
438            "The parameter cannot be converted to a numeric value".to_string(),
439        ));
440    }
441
442    let negative = trimmed.starts_with('-');
443    let abs_str = if negative { &trimmed[1..] } else { trimmed };
444
445    // Validate that the string is a well-formed number: must contain at least one digit,
446    // and only valid number characters (digits, '.', 'e'/'E', '+', '-' in exponent).
447    // Rejects "NaN", "Infinity", "abc", etc.
448    if abs_str.is_empty() || !abs_str.chars().any(|c| c.is_ascii_digit()) {
449        return Err(crate::errors::DynoxideError::ValidationException(format!(
450            "The parameter cannot be converted to a numeric value: {}",
451            trimmed
452        )));
453    }
454    let valid = abs_str.chars().enumerate().all(|(i, c)| {
455        c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || ((c == '+' || c == '-') && i > 0) // sign only after 'e'/'E'
456    });
457    if !valid {
458        return Err(crate::errors::DynoxideError::ValidationException(format!(
459            "The parameter cannot be converted to a numeric value: {}",
460            trimmed
461        )));
462    }
463
464    let (mantissa_digits, exponent) = parse_number_parts(abs_str);
465
466    // Zero is always valid
467    if mantissa_digits.is_empty() || mantissa_digits.iter().all(|&d| d == 0) {
468        return Ok(());
469    }
470
471    // Check significant digits (mantissa_digits has leading/trailing zeros already stripped)
472    if mantissa_digits.len() > 38 {
473        return Err(crate::errors::DynoxideError::ValidationException(
474            "Attempting to store more than 38 significant digits in a Number".to_string(),
475        ));
476    }
477
478    // Check magnitude: exponent represents the power such that value = 0.mantissa * 10^exponent
479    // Max magnitude: 9.999...E+125 means exponent = 126 (since 0.999... * 10^126 = 9.99...E+125)
480    if exponent > 126 {
481        return Err(crate::errors::DynoxideError::ValidationException(
482            "Number overflow. Attempting to store a number with magnitude larger than supported range"
483                .to_string(),
484        ));
485    }
486
487    // Check underflow for non-zero values
488    // Min positive: 1E-130 means exponent = -129 (since 0.1 * 10^-129 = 1E-130)
489    // But with more digits, exponent can be lower, e.g. 1.0E-130 has (mantissa=[1], exponent=-129)
490    // Actually, the smallest representable is 1E-130. In our representation, 1E-130 = 0.1 * 10^-129
491    // So exponent = -129 with mantissa [1].
492    // For 1E-131 = 0.1 * 10^-130, exponent = -130 — that's too small.
493    if exponent < -129 {
494        return Err(crate::errors::DynoxideError::ValidationException(
495            "Number underflow. Attempting to store a number with magnitude smaller than supported range"
496                .to_string(),
497        ));
498    }
499
500    Ok(())
501}
502
503/// Normalize a DynamoDB number string to its canonical form.
504///
505/// DynamoDB normalises numbers when storing them:
506/// - Leading zeros are stripped (`0042` → `42`)
507/// - Trailing zeros after decimal are stripped (`1.200` → `1.2`)
508/// - Scientific notation is expanded to full decimal form
509/// - Zero is represented as `0`
510pub fn normalize_dynamo_number(num_str: &str) -> String {
511    let trimmed = num_str.trim();
512    if trimmed.is_empty() {
513        return "0".to_string();
514    }
515
516    let negative = trimmed.starts_with('-');
517    let abs_str = if negative {
518        &trimmed[1..]
519    } else {
520        trimmed.trim_start_matches('+')
521    };
522
523    let (mantissa_digits, exponent) = parse_number_parts(abs_str);
524
525    // Zero
526    if mantissa_digits.is_empty() {
527        return "0".to_string();
528    }
529
530    // Reconstruct: mantissa_digits represent the significant digits,
531    // exponent is the power of 10 such that value = 0.mantissa * 10^exponent
532    // e.g., 12345 → mantissa=[1,2,3,4,5], exponent=5 → 12345
533    // e.g., 0.00123 → mantissa=[1,2,3], exponent=-2 → 0.00123
534    let num_digits = mantissa_digits.len() as i32;
535    let int_digits = exponent; // number of digits before the decimal point
536
537    let mut result = String::new();
538    if negative {
539        result.push('-');
540    }
541
542    if int_digits <= 0 {
543        // Pure fraction: 0.000...digits
544        result.push_str("0.");
545        for _ in 0..(-int_digits) {
546            result.push('0');
547        }
548        for &d in &mantissa_digits {
549            result.push((b'0' + d) as char);
550        }
551    } else if int_digits >= num_digits {
552        // Pure integer: digits followed by trailing zeros
553        for &d in &mantissa_digits {
554            result.push((b'0' + d) as char);
555        }
556        for _ in 0..(int_digits - num_digits) {
557            result.push('0');
558        }
559    } else {
560        // Mixed: some digits before decimal, some after
561        let int_part = int_digits as usize;
562        for &d in &mantissa_digits[..int_part] {
563            result.push((b'0' + d) as char);
564        }
565        result.push('.');
566        for &d in &mantissa_digits[int_part..] {
567            result.push((b'0' + d) as char);
568        }
569    }
570
571    result
572}
573
574fn zero_encoding() -> String {
575    // Zero sorts between negative (prefix "0") and positive (prefix "2")
576    format!("1{}{}", "0".repeat(4), "0".repeat(40))
577}
578
579fn encode_positive(mantissa: &[u8], exponent: i32) -> String {
580    let exp_encoded = (exponent + 5000) as u16;
581    let mantissa_str = mantissa_to_string(mantissa, 40);
582    format!("2{exp_encoded:04}{mantissa_str}")
583}
584
585fn encode_negative(mantissa: &[u8], exponent: i32) -> String {
586    // For negatives, we complement everything so larger absolute values sort first (smaller)
587    let exp_encoded = 9999 - (exponent + 5000) as u16;
588    let mantissa_str = complement_mantissa(mantissa, 40);
589    format!("0{exp_encoded:04}{mantissa_str}")
590}
591
592/// Parse a non-negative number string into (mantissa digits, exponent).
593/// Mantissa is normalized: first digit is non-zero, exponent is the power of 10
594/// such that the number = 0.mantissa * 10^exponent.
595pub(crate) fn parse_number_parts(s: &str) -> (Vec<u8>, i32) {
596    // Handle scientific notation
597    let (coeff, exp_part) = if let Some(pos) = s.to_ascii_lowercase().find('e') {
598        let coeff = &s[..pos];
599        let exp: i32 = s[pos + 1..].parse().unwrap_or(0);
600        (coeff, exp)
601    } else {
602        (s, 0)
603    };
604
605    // Split coefficient into integer and fraction parts
606    let (int_part, frac_part) = if let Some(dot) = coeff.find('.') {
607        (&coeff[..dot], &coeff[dot + 1..])
608    } else {
609        (coeff, "")
610    };
611
612    // Collect all digits
613    let mut digits: Vec<u8> = Vec::new();
614    for ch in int_part.chars().chain(frac_part.chars()) {
615        if ch.is_ascii_digit() {
616            digits.push(ch as u8 - b'0');
617        }
618    }
619
620    if digits.is_empty() {
621        return (vec![], 0);
622    }
623
624    // The integer part length gives us the base exponent
625    let int_len = int_part.chars().filter(|c| c.is_ascii_digit()).count() as i32;
626
627    // Find first non-zero digit
628    let leading_zeros = digits.iter().take_while(|&&d| d == 0).count();
629    digits.drain(..leading_zeros);
630
631    // Trim trailing zeros
632    while digits.last() == Some(&0) {
633        digits.pop();
634    }
635
636    if digits.is_empty() {
637        return (vec![], 0);
638    }
639
640    // exponent = int_len - leading_zeros + exp_part
641    // But we need to account for whether leading zeros were in int or frac part
642    let exponent = int_len - leading_zeros as i32 + exp_part;
643
644    (digits, exponent)
645}
646
647fn mantissa_to_string(digits: &[u8], width: usize) -> String {
648    let mut s = String::with_capacity(width);
649    for &d in digits.iter().take(width) {
650        s.push((b'0' + d) as char);
651    }
652    while s.len() < width {
653        s.push('0');
654    }
655    s
656}
657
658fn complement_mantissa(digits: &[u8], width: usize) -> String {
659    let mut s = String::with_capacity(width);
660    for i in 0..width {
661        let d = if i < digits.len() { digits[i] } else { 0 };
662        s.push((b'0' + (9 - d)) as char);
663    }
664    s
665}
666
667/// Hex-encode bytes (lowercase) for binary key storage.
668fn hex_encode(bytes: &[u8]) -> String {
669    let mut s = String::with_capacity(bytes.len() * 2);
670    for &b in bytes {
671        s.push_str(&format!("{b:02x}"));
672    }
673    s
674}
675
676// ---------------------------------------------------------------------------
677// Item helpers
678// ---------------------------------------------------------------------------
679
680/// A DynamoDB item: a map of attribute names to values.
681pub type Item = HashMap<String, AttributeValue>;
682
683/// SSE specification for server-side encryption settings.
684#[derive(Debug, Clone, Default, Serialize, Deserialize)]
685pub struct SseSpecification {
686    #[serde(rename = "Enabled", default)]
687    pub enabled: Option<bool>,
688    #[serde(rename = "SSEType", default)]
689    pub sse_type: Option<String>,
690    #[serde(rename = "KMSMasterKeyId", default)]
691    pub kms_master_key_id: Option<String>,
692}
693
694/// DynamoDB Tag (key-value pair attached to a resource).
695#[derive(Debug, Clone, Default, Serialize, Deserialize)]
696pub struct Tag {
697    #[serde(rename = "Key")]
698    pub key: String,
699    #[serde(rename = "Value")]
700    pub value: String,
701}
702
703/// Calculate the total size of a DynamoDB item in bytes.
704pub fn item_size(item: &Item) -> usize {
705    item.iter()
706        .map(|(name, value)| name.len() + value.size())
707        .sum()
708}
709
710/// Maximum item size in bytes (400 KB).
711pub const MAX_ITEM_SIZE: usize = 400 * 1024;
712
713/// ItemCollectionMetrics returned when `ReturnItemCollectionMetrics: SIZE` is set
714/// and the table has local secondary indexes.
715#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct ItemCollectionMetrics {
717    #[serde(rename = "ItemCollectionKey")]
718    pub item_collection_key: HashMap<String, AttributeValue>,
719    #[serde(rename = "SizeEstimateRangeGB")]
720    pub size_estimate_range_gb: Vec<f64>,
721}
722
723/// ConsumedCapacity returned when `ReturnConsumedCapacity` is set.
724#[derive(Debug, Clone, Default, Serialize, Deserialize)]
725pub struct ConsumedCapacity {
726    #[serde(rename = "TableName")]
727    pub table_name: String,
728    #[serde(rename = "CapacityUnits")]
729    pub capacity_units: f64,
730    #[serde(rename = "Table", skip_serializing_if = "Option::is_none")]
731    pub table: Option<CapacityDetail>,
732    #[serde(
733        rename = "GlobalSecondaryIndexes",
734        skip_serializing_if = "Option::is_none"
735    )]
736    pub global_secondary_indexes: Option<HashMap<String, CapacityDetail>>,
737    #[serde(
738        rename = "LocalSecondaryIndexes",
739        skip_serializing_if = "Option::is_none"
740    )]
741    pub local_secondary_indexes: Option<HashMap<String, CapacityDetail>>,
742}
743
744/// Per-resource capacity detail.
745#[derive(Debug, Clone, Default, Serialize, Deserialize)]
746pub struct CapacityDetail {
747    #[serde(rename = "CapacityUnits")]
748    pub capacity_units: f64,
749    #[serde(rename = "ReadCapacityUnits", skip_serializing_if = "Option::is_none")]
750    pub read_capacity_units: Option<f64>,
751    #[serde(rename = "WriteCapacityUnits", skip_serializing_if = "Option::is_none")]
752    pub write_capacity_units: Option<f64>,
753}
754
755/// The transactional capacity multiplier. `TransactWriteItems` and
756/// `TransactGetItems` cost twice the equivalent single-item operation, so each
757/// item's rounded-up units are doubled (the rounding happens per item, before
758/// the multiplier, to match AWS at the KB/4KB boundary).
759pub const TRANSACTIONAL_CAPACITY_FACTOR: f64 = 2.0;
760
761/// Calculate write capacity units (1 WCU = 1KB, rounded up).
762pub fn write_capacity_units(item_size_bytes: usize) -> f64 {
763    ((item_size_bytes as f64) / 1024.0).ceil().max(1.0)
764}
765
766/// Calculate read capacity units assuming strongly consistent reads
767/// (1 RCU per 4KB, rounded up). Used when ConsistentRead is true or
768/// when the read type is not specified.
769pub fn read_capacity_units(item_size_bytes: usize) -> f64 {
770    ((item_size_bytes as f64) / 4096.0).ceil().max(1.0)
771}
772
773/// Calculate read capacity units accounting for consistency mode.
774///
775/// Strongly consistent: 1 RCU per 4KB, rounded up.
776/// Eventually consistent: 0.5 RCU per 4KB (half the strongly consistent rate).
777pub fn read_capacity_units_with_consistency(item_size_bytes: usize, consistent: bool) -> f64 {
778    let strongly = read_capacity_units(item_size_bytes);
779    if consistent { strongly } else { strongly / 2.0 }
780}
781
782/// Build a `ConsumedCapacity` for a simple table operation.
783pub fn consumed_capacity(
784    table_name: &str,
785    capacity_units: f64,
786    mode: &Option<String>,
787) -> Option<ConsumedCapacity> {
788    let mode = mode.as_deref().unwrap_or("NONE");
789    match mode {
790        "TOTAL" => Some(ConsumedCapacity {
791            table_name: table_name.to_string(),
792            capacity_units,
793            table: None,
794            global_secondary_indexes: None,
795            local_secondary_indexes: None,
796        }),
797        "INDEXES" => Some(ConsumedCapacity {
798            table_name: table_name.to_string(),
799            capacity_units,
800            table: Some(CapacityDetail {
801                capacity_units,
802                ..Default::default()
803            }),
804            global_secondary_indexes: None,
805            local_secondary_indexes: None,
806        }),
807        _ => None,
808    }
809}
810
811/// Build a `ConsumedCapacity` with per-GSI breakdown for INDEXES mode.
812pub fn consumed_capacity_with_indexes(
813    table_name: &str,
814    table_units: f64,
815    gsi_units: &HashMap<String, f64>,
816    mode: &Option<String>,
817) -> Option<ConsumedCapacity> {
818    consumed_capacity_with_secondary_indexes(
819        table_name,
820        table_units,
821        gsi_units,
822        &HashMap::new(),
823        mode,
824    )
825}
826
827/// Build a `ConsumedCapacity` with per-GSI and per-LSI breakdown for INDEXES mode.
828pub fn consumed_capacity_with_secondary_indexes(
829    table_name: &str,
830    table_units: f64,
831    gsi_units: &HashMap<String, f64>,
832    lsi_units: &HashMap<String, f64>,
833    mode: &Option<String>,
834) -> Option<ConsumedCapacity> {
835    let units_to_map = |units: &HashMap<String, f64>| -> Option<HashMap<String, CapacityDetail>> {
836        if units.is_empty() {
837            None
838        } else {
839            Some(
840                units
841                    .iter()
842                    .map(|(name, &u)| {
843                        (
844                            name.clone(),
845                            CapacityDetail {
846                                capacity_units: u,
847                                ..Default::default()
848                            },
849                        )
850                    })
851                    .collect(),
852            )
853        }
854    };
855
856    match mode.as_deref().unwrap_or("NONE") {
857        "INDEXES" => {
858            let gsi_total: f64 = gsi_units.values().sum();
859            let lsi_total: f64 = lsi_units.values().sum();
860            Some(ConsumedCapacity {
861                table_name: table_name.to_string(),
862                capacity_units: table_units + gsi_total + lsi_total,
863                table: Some(CapacityDetail {
864                    capacity_units: table_units,
865                    ..Default::default()
866                }),
867                global_secondary_indexes: units_to_map(gsi_units),
868                local_secondary_indexes: units_to_map(lsi_units),
869            })
870        }
871        "TOTAL" => {
872            let gsi_total: f64 = gsi_units.values().sum();
873            let lsi_total: f64 = lsi_units.values().sum();
874            Some(ConsumedCapacity {
875                table_name: table_name.to_string(),
876                capacity_units: table_units + gsi_total + lsi_total,
877                table: None,
878                global_secondary_indexes: None,
879                local_secondary_indexes: None,
880            })
881        }
882        _ => None,
883    }
884}
885
886/// Build a `ConsumedCapacity` for one table in a transactional read
887/// (`TransactGetItems`). `units` is the table total and already includes the
888/// transactional 2x factor. Under `INDEXES` the Table detail reports
889/// `ReadCapacityUnits` alongside `CapacityUnits`, matching AWS.
890pub fn transactional_read_capacity(
891    table_name: &str,
892    units: f64,
893    mode: &Option<String>,
894) -> Option<ConsumedCapacity> {
895    match mode.as_deref().unwrap_or("NONE") {
896        "TOTAL" => Some(ConsumedCapacity {
897            table_name: table_name.to_string(),
898            capacity_units: units,
899            ..Default::default()
900        }),
901        "INDEXES" => Some(ConsumedCapacity {
902            table_name: table_name.to_string(),
903            capacity_units: units,
904            table: Some(CapacityDetail {
905                capacity_units: units,
906                read_capacity_units: Some(units),
907                ..Default::default()
908            }),
909            ..Default::default()
910        }),
911        _ => None,
912    }
913}
914
915/// Build a `ConsumedCapacity` for one table in a transactional write
916/// (`TransactWriteItems`). `units` is the table total and already includes the
917/// transactional 2x factor. Under `INDEXES` the Table detail reports
918/// `WriteCapacityUnits` alongside `CapacityUnits`, matching AWS.
919pub fn transactional_write_capacity(
920    table_name: &str,
921    units: f64,
922    mode: &Option<String>,
923) -> Option<ConsumedCapacity> {
924    match mode.as_deref().unwrap_or("NONE") {
925        "TOTAL" => Some(ConsumedCapacity {
926            table_name: table_name.to_string(),
927            capacity_units: units,
928            ..Default::default()
929        }),
930        "INDEXES" => Some(ConsumedCapacity {
931            table_name: table_name.to_string(),
932            capacity_units: units,
933            table: Some(CapacityDetail {
934                capacity_units: units,
935                write_capacity_units: Some(units),
936                ..Default::default()
937            }),
938            ..Default::default()
939        }),
940        _ => None,
941    }
942}
943
944/// Key schema element — defines a key attribute.
945#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
946pub struct KeySchemaElement {
947    #[serde(rename = "AttributeName", alias = "attribute_name")]
948    pub attribute_name: String,
949    #[serde(rename = "KeyType", alias = "key_type")]
950    pub key_type: KeyType,
951}
952
953/// Key type: HASH (partition key) or RANGE (sort key).
954#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
955pub enum KeyType {
956    #[default]
957    HASH,
958    RANGE,
959}
960
961/// Attribute definition — declares an attribute's type.
962#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
963pub struct AttributeDefinition {
964    #[serde(rename = "AttributeName", alias = "attribute_name")]
965    pub attribute_name: String,
966    #[serde(rename = "AttributeType", alias = "attribute_type")]
967    pub attribute_type: ScalarAttributeType,
968}
969
970/// Scalar attribute types that can be used as keys.
971#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
972pub enum ScalarAttributeType {
973    #[default]
974    S,
975    N,
976    B,
977}
978
979/// GSI projection type.
980#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
981pub struct Projection {
982    #[serde(
983        rename = "ProjectionType",
984        alias = "projection_type",
985        default,
986        skip_serializing_if = "Option::is_none"
987    )]
988    pub projection_type: Option<ProjectionType>,
989    #[serde(
990        rename = "NonKeyAttributes",
991        alias = "non_key_attributes",
992        skip_serializing_if = "Option::is_none"
993    )]
994    pub non_key_attributes: Option<Vec<String>>,
995}
996
997/// Projection type enum.
998#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
999#[allow(non_camel_case_types)]
1000pub enum ProjectionType {
1001    #[default]
1002    ALL,
1003    KEYS_ONLY,
1004    INCLUDE,
1005}
1006
1007/// Global Secondary Index definition.
1008#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1009pub struct GlobalSecondaryIndex {
1010    #[serde(rename = "IndexName", alias = "index_name")]
1011    pub index_name: String,
1012    #[serde(rename = "KeySchema", alias = "key_schema")]
1013    pub key_schema: Vec<KeySchemaElement>,
1014    #[serde(rename = "Projection", alias = "projection")]
1015    pub projection: Projection,
1016    #[serde(
1017        rename = "ProvisionedThroughput",
1018        alias = "provisioned_throughput",
1019        skip_serializing_if = "Option::is_none"
1020    )]
1021    pub provisioned_throughput: Option<ProvisionedThroughput>,
1022}
1023
1024/// Local Secondary Index definition.
1025#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1026pub struct LocalSecondaryIndex {
1027    #[serde(rename = "IndexName", alias = "index_name")]
1028    pub index_name: String,
1029    #[serde(rename = "KeySchema", alias = "key_schema")]
1030    pub key_schema: Vec<KeySchemaElement>,
1031    #[serde(rename = "Projection", alias = "projection")]
1032    pub projection: Projection,
1033}
1034
1035/// Provisioned throughput settings (stored but not enforced).
1036#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1037pub struct ProvisionedThroughput {
1038    #[serde(rename = "ReadCapacityUnits", default)]
1039    pub read_capacity_units: Option<i64>,
1040    #[serde(rename = "WriteCapacityUnits", default)]
1041    pub write_capacity_units: Option<i64>,
1042}
1043
1044/// On-demand (PAY_PER_REQUEST) throughput ceilings (stored but not enforced).
1045#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1046pub struct OnDemandThroughput {
1047    #[serde(
1048        rename = "MaxReadRequestUnits",
1049        default,
1050        skip_serializing_if = "Option::is_none"
1051    )]
1052    pub max_read_request_units: Option<i64>,
1053    #[serde(
1054        rename = "MaxWriteRequestUnits",
1055        default,
1056        skip_serializing_if = "Option::is_none"
1057    )]
1058    pub max_write_request_units: Option<i64>,
1059}
1060
1061// ---------------------------------------------------------------------------
1062// Type conversion: From<T> / TryFrom<T> for AttributeValue
1063// ---------------------------------------------------------------------------
1064
1065/// Error returned when converting between `AttributeValue` and Rust types.
1066#[derive(Debug, Clone, PartialEq)]
1067pub struct ConversionError {
1068    /// The expected DynamoDB or Rust type.
1069    pub expected: &'static str,
1070    /// The actual DynamoDB type encountered.
1071    pub actual: &'static str,
1072}
1073
1074impl fmt::Display for ConversionError {
1075    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1076        write!(f, "expected {}, got {}", self.expected, self.actual)
1077    }
1078}
1079
1080impl std::error::Error for ConversionError {}
1081
1082// --- From<T> for AttributeValue: infallible conversions ---
1083
1084impl From<String> for AttributeValue {
1085    fn from(value: String) -> Self {
1086        AttributeValue::S(value)
1087    }
1088}
1089
1090impl From<&str> for AttributeValue {
1091    fn from(value: &str) -> Self {
1092        AttributeValue::S(value.to_string())
1093    }
1094}
1095
1096impl From<bool> for AttributeValue {
1097    fn from(value: bool) -> Self {
1098        AttributeValue::BOOL(value)
1099    }
1100}
1101
1102impl From<Vec<u8>> for AttributeValue {
1103    fn from(value: Vec<u8>) -> Self {
1104        AttributeValue::B(value)
1105    }
1106}
1107
1108impl From<&[u8]> for AttributeValue {
1109    fn from(value: &[u8]) -> Self {
1110        AttributeValue::B(value.to_vec())
1111    }
1112}
1113
1114// Integer types — all finite, all fit in DynamoDB's number range.
1115macro_rules! impl_from_integer {
1116    ($($t:ty),+) => {
1117        $(
1118            impl From<$t> for AttributeValue {
1119                fn from(value: $t) -> Self {
1120                    AttributeValue::N(value.to_string())
1121                }
1122            }
1123        )+
1124    };
1125}
1126
1127impl_from_integer!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
1128
1129// Container types
1130impl From<HashMap<String, AttributeValue>> for AttributeValue {
1131    fn from(value: HashMap<String, AttributeValue>) -> Self {
1132        AttributeValue::M(value)
1133    }
1134}
1135
1136impl From<Vec<AttributeValue>> for AttributeValue {
1137    fn from(value: Vec<AttributeValue>) -> Self {
1138        AttributeValue::L(value)
1139    }
1140}
1141
1142impl From<HashSet<String>> for AttributeValue {
1143    fn from(value: HashSet<String>) -> Self {
1144        AttributeValue::SS(value.into_iter().collect())
1145    }
1146}
1147
1148impl From<BTreeSet<String>> for AttributeValue {
1149    fn from(value: BTreeSet<String>) -> Self {
1150        AttributeValue::SS(value.into_iter().collect())
1151    }
1152}
1153
1154// --- TryFrom<T> for AttributeValue: fallible conversions (floats) ---
1155
1156impl TryFrom<f64> for AttributeValue {
1157    type Error = ConversionError;
1158
1159    fn try_from(value: f64) -> std::result::Result<Self, Self::Error> {
1160        if value.is_finite() {
1161            Ok(AttributeValue::N(value.to_string()))
1162        } else {
1163            Err(ConversionError {
1164                expected: "finite f64",
1165                actual: "NaN or Infinity",
1166            })
1167        }
1168    }
1169}
1170
1171impl TryFrom<f32> for AttributeValue {
1172    type Error = ConversionError;
1173
1174    fn try_from(value: f32) -> std::result::Result<Self, Self::Error> {
1175        if value.is_finite() {
1176            Ok(AttributeValue::N(value.to_string()))
1177        } else {
1178            Err(ConversionError {
1179                expected: "finite f32",
1180                actual: "NaN or Infinity",
1181            })
1182        }
1183    }
1184}
1185
1186// --- TryFrom<AttributeValue> for T: extract Rust types from AV ---
1187
1188impl TryFrom<AttributeValue> for String {
1189    type Error = ConversionError;
1190
1191    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1192        match value {
1193            AttributeValue::S(s) => Ok(s),
1194            other => Err(ConversionError {
1195                expected: "S",
1196                actual: other.type_name(),
1197            }),
1198        }
1199    }
1200}
1201
1202impl TryFrom<AttributeValue> for bool {
1203    type Error = ConversionError;
1204
1205    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1206        match value {
1207            AttributeValue::BOOL(b) => Ok(b),
1208            other => Err(ConversionError {
1209                expected: "BOOL",
1210                actual: other.type_name(),
1211            }),
1212        }
1213    }
1214}
1215
1216impl TryFrom<AttributeValue> for Vec<u8> {
1217    type Error = ConversionError;
1218
1219    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1220        match value {
1221            AttributeValue::B(b) => Ok(b),
1222            other => Err(ConversionError {
1223                expected: "B",
1224                actual: other.type_name(),
1225            }),
1226        }
1227    }
1228}
1229
1230macro_rules! impl_try_from_av_integer {
1231    ($($t:ty),+) => {
1232        $(
1233            impl TryFrom<AttributeValue> for $t {
1234                type Error = ConversionError;
1235
1236                fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1237                    match value {
1238                        AttributeValue::N(n) => n.parse::<$t>().map_err(|_| ConversionError {
1239                            expected: stringify!($t),
1240                            actual: "N (parse failed)",
1241                        }),
1242                        other => Err(ConversionError {
1243                            expected: "N",
1244                            actual: other.type_name(),
1245                        }),
1246                    }
1247                }
1248            }
1249        )+
1250    };
1251}
1252
1253impl_try_from_av_integer!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
1254
1255impl TryFrom<AttributeValue> for f64 {
1256    type Error = ConversionError;
1257
1258    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1259        match value {
1260            AttributeValue::N(n) => n.parse::<f64>().map_err(|_| ConversionError {
1261                expected: "f64",
1262                actual: "N (parse failed)",
1263            }),
1264            other => Err(ConversionError {
1265                expected: "N",
1266                actual: other.type_name(),
1267            }),
1268        }
1269    }
1270}
1271
1272impl TryFrom<AttributeValue> for f32 {
1273    type Error = ConversionError;
1274
1275    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1276        match value {
1277            AttributeValue::N(n) => n.parse::<f32>().map_err(|_| ConversionError {
1278                expected: "f32",
1279                actual: "N (parse failed)",
1280            }),
1281            other => Err(ConversionError {
1282                expected: "N",
1283                actual: other.type_name(),
1284            }),
1285        }
1286    }
1287}
1288
1289impl TryFrom<AttributeValue> for HashMap<String, AttributeValue> {
1290    type Error = ConversionError;
1291
1292    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1293        match value {
1294            AttributeValue::M(m) => Ok(m),
1295            other => Err(ConversionError {
1296                expected: "M",
1297                actual: other.type_name(),
1298            }),
1299        }
1300    }
1301}
1302
1303impl TryFrom<AttributeValue> for Vec<AttributeValue> {
1304    type Error = ConversionError;
1305
1306    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1307        match value {
1308            AttributeValue::L(l) => Ok(l),
1309            other => Err(ConversionError {
1310                expected: "L",
1311                actual: other.type_name(),
1312            }),
1313        }
1314    }
1315}
1316
1317impl TryFrom<AttributeValue> for Vec<String> {
1318    type Error = ConversionError;
1319
1320    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1321        match value {
1322            AttributeValue::SS(ss) => Ok(ss),
1323            AttributeValue::L(l) => {
1324                // Lenient: extract S values from a list
1325                l.into_iter()
1326                    .map(|av| match av {
1327                        AttributeValue::S(s) => Ok(s),
1328                        other => Err(ConversionError {
1329                            expected: "S (within L)",
1330                            actual: other.type_name(),
1331                        }),
1332                    })
1333                    .collect()
1334            }
1335            other => Err(ConversionError {
1336                expected: "SS or L",
1337                actual: other.type_name(),
1338            }),
1339        }
1340    }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345    use super::*;
1346
1347    #[test]
1348    fn test_serialize_string() {
1349        let val = AttributeValue::S("hello".to_string());
1350        let json = serde_json::to_string(&val).unwrap();
1351        assert_eq!(json, r#"{"S":"hello"}"#);
1352    }
1353
1354    #[test]
1355    fn test_serialize_number() {
1356        let val = AttributeValue::N("42".to_string());
1357        let json = serde_json::to_string(&val).unwrap();
1358        assert_eq!(json, r#"{"N":"42"}"#);
1359    }
1360
1361    #[test]
1362    fn test_serialize_binary() {
1363        let val = AttributeValue::B(vec![1, 2, 3]);
1364        let json = serde_json::to_string(&val).unwrap();
1365        assert_eq!(json, r#"{"B":"AQID"}"#);
1366    }
1367
1368    #[test]
1369    fn test_serialize_bool() {
1370        let val = AttributeValue::BOOL(true);
1371        let json = serde_json::to_string(&val).unwrap();
1372        assert_eq!(json, r#"{"BOOL":true}"#);
1373    }
1374
1375    #[test]
1376    fn test_serialize_null() {
1377        let val = AttributeValue::NULL(true);
1378        let json = serde_json::to_string(&val).unwrap();
1379        assert_eq!(json, r#"{"NULL":true}"#);
1380    }
1381
1382    #[test]
1383    fn test_deserialize_null_true() {
1384        let val: AttributeValue = serde_json::from_str(r#"{"NULL":true}"#).unwrap();
1385        assert_eq!(val, AttributeValue::NULL(true));
1386    }
1387
1388    #[test]
1389    fn test_deserialize_null_false_normalises_to_true() {
1390        // The NULL member is a plain boolean in the model, so {"NULL": false} is
1391        // valid input. AWS accepts it and reads it back as {"NULL": true}, so
1392        // normalise false to true on the way in.
1393        let val: AttributeValue = serde_json::from_str(r#"{"NULL":false}"#).unwrap();
1394        assert_eq!(val, AttributeValue::NULL(true));
1395    }
1396
1397    #[test]
1398    fn test_deserialize_null_non_boolean_rejected() {
1399        // A non-boolean NULL (e.g. {"NULL": "no"}) is a type error, not a value.
1400        let err = serde_json::from_str::<AttributeValue>(r#"{"NULL":"no"}"#).unwrap_err();
1401        assert!(
1402            err.to_string().contains("must have the value of true"),
1403            "unexpected error: {err}"
1404        );
1405    }
1406
1407    #[test]
1408    fn test_serialize_string_set() {
1409        let val = AttributeValue::SS(vec!["a".to_string(), "b".to_string()]);
1410        let json = serde_json::to_string(&val).unwrap();
1411        assert_eq!(json, r#"{"SS":["a","b"]}"#);
1412    }
1413
1414    #[test]
1415    fn test_serialize_list() {
1416        let val = AttributeValue::L(vec![
1417            AttributeValue::S("hello".to_string()),
1418            AttributeValue::N("42".to_string()),
1419        ]);
1420        let json = serde_json::to_string(&val).unwrap();
1421        assert_eq!(json, r#"{"L":[{"S":"hello"},{"N":"42"}]}"#);
1422    }
1423
1424    #[test]
1425    fn test_serialize_map() {
1426        let mut m = HashMap::new();
1427        m.insert("key".to_string(), AttributeValue::S("value".to_string()));
1428        let val = AttributeValue::M(m);
1429        let json = serde_json::to_string(&val).unwrap();
1430        assert_eq!(json, r#"{"M":{"key":{"S":"value"}}}"#);
1431    }
1432
1433    #[test]
1434    fn test_round_trip_all_types() {
1435        let values = vec![
1436            AttributeValue::S("hello".to_string()),
1437            AttributeValue::N("42.5".to_string()),
1438            AttributeValue::B(vec![0, 255, 128]),
1439            AttributeValue::BOOL(false),
1440            AttributeValue::NULL(true),
1441            AttributeValue::SS(vec!["x".to_string(), "y".to_string()]),
1442            AttributeValue::NS(vec!["1".to_string(), "2.5".to_string()]),
1443            AttributeValue::BS(vec![vec![1], vec![2, 3]]),
1444            AttributeValue::L(vec![
1445                AttributeValue::S("nested".to_string()),
1446                AttributeValue::N("99".to_string()),
1447            ]),
1448        ];
1449
1450        for val in values {
1451            let json = serde_json::to_string(&val).unwrap();
1452            let deserialized: AttributeValue = serde_json::from_str(&json).unwrap();
1453            assert_eq!(val, deserialized, "Round-trip failed for {json}");
1454        }
1455    }
1456
1457    #[test]
1458    fn test_size_string() {
1459        let val = AttributeValue::S("hello".to_string());
1460        assert_eq!(val.size(), 5);
1461    }
1462
1463    #[test]
1464    fn test_size_number() {
1465        // "42" has 2 significant digits → (2/2) + 1 = 2
1466        let val = AttributeValue::N("42".to_string());
1467        assert_eq!(val.size(), 2);
1468    }
1469
1470    #[test]
1471    fn test_size_bool() {
1472        assert_eq!(AttributeValue::BOOL(true).size(), 1);
1473    }
1474
1475    #[test]
1476    fn test_size_null() {
1477        assert_eq!(AttributeValue::NULL(true).size(), 1);
1478    }
1479
1480    #[test]
1481    fn test_key_string_s() {
1482        let val = AttributeValue::S("hello".to_string());
1483        assert_eq!(val.to_key_string(), Some("S:hello".to_string()));
1484    }
1485
1486    #[test]
1487    fn test_key_string_n() {
1488        let val = AttributeValue::N("42".to_string());
1489        let key = val.to_key_string().unwrap();
1490        assert!(key.starts_with("N:"));
1491    }
1492
1493    #[test]
1494    fn test_key_string_b() {
1495        let val = AttributeValue::B(vec![0xff, 0x00, 0xab]);
1496        assert_eq!(val.to_key_string(), Some("B:ff00ab".to_string()));
1497    }
1498
1499    #[test]
1500    fn test_key_string_non_key_type_returns_none() {
1501        assert_eq!(AttributeValue::BOOL(true).to_key_string(), None);
1502        assert_eq!(AttributeValue::L(vec![]).to_key_string(), None);
1503    }
1504
1505    // Number sort key ordering tests
1506    #[test]
1507    fn test_number_sort_ordering() {
1508        let numbers = vec![
1509            "-1000", "-100", "-10", "-1", "-0.5", "-0.001", "0", "0.001", "0.5", "1", "10", "100",
1510            "1000",
1511        ];
1512        let encoded: Vec<String> = numbers
1513            .iter()
1514            .map(|n| normalize_number_for_sort(n))
1515            .collect();
1516
1517        for i in 0..encoded.len() - 1 {
1518            assert!(
1519                encoded[i] < encoded[i + 1],
1520                "Sort order broken: {} ({}) should be < {} ({})",
1521                numbers[i],
1522                encoded[i],
1523                numbers[i + 1],
1524                encoded[i + 1]
1525            );
1526        }
1527    }
1528
1529    #[test]
1530    fn test_number_sort_zero_variants() {
1531        let z1 = normalize_number_for_sort("0");
1532        let z2 = normalize_number_for_sort("-0");
1533        let z3 = normalize_number_for_sort("0.0");
1534        assert_eq!(z1, z2);
1535        assert_eq!(z2, z3);
1536    }
1537
1538    #[test]
1539    fn test_number_sort_decimals() {
1540        let a = normalize_number_for_sort("1.5");
1541        let b = normalize_number_for_sort("2.5");
1542        assert!(a < b);
1543
1544        let c = normalize_number_for_sort("0.001");
1545        let d = normalize_number_for_sort("0.01");
1546        assert!(c < d);
1547    }
1548
1549    #[test]
1550    fn test_number_sort_scientific() {
1551        let a = normalize_number_for_sort("1e10");
1552        let b = normalize_number_for_sort("1e11");
1553        assert!(a < b);
1554
1555        let c = normalize_number_for_sort("-1e11");
1556        let d = normalize_number_for_sort("-1e10");
1557        assert!(c < d);
1558    }
1559
1560    #[test]
1561    fn test_type_name() {
1562        assert_eq!(AttributeValue::S("".to_string()).type_name(), "S");
1563        assert_eq!(AttributeValue::N("0".to_string()).type_name(), "N");
1564        assert_eq!(AttributeValue::B(vec![]).type_name(), "B");
1565        assert_eq!(AttributeValue::BOOL(true).type_name(), "BOOL");
1566        assert_eq!(AttributeValue::NULL(true).type_name(), "NULL");
1567        assert_eq!(AttributeValue::SS(vec![]).type_name(), "SS");
1568        assert_eq!(AttributeValue::NS(vec![]).type_name(), "NS");
1569        assert_eq!(AttributeValue::BS(vec![]).type_name(), "BS");
1570        assert_eq!(AttributeValue::L(vec![]).type_name(), "L");
1571        assert_eq!(AttributeValue::M(HashMap::new()).type_name(), "M");
1572    }
1573}