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