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}
743
744/// Calculate write capacity units (1 WCU = 1KB, rounded up).
745pub fn write_capacity_units(item_size_bytes: usize) -> f64 {
746    ((item_size_bytes as f64) / 1024.0).ceil().max(1.0)
747}
748
749/// Calculate read capacity units assuming strongly consistent reads
750/// (1 RCU per 4KB, rounded up). Used when ConsistentRead is true or
751/// when the read type is not specified.
752pub fn read_capacity_units(item_size_bytes: usize) -> f64 {
753    ((item_size_bytes as f64) / 4096.0).ceil().max(1.0)
754}
755
756/// Calculate read capacity units accounting for consistency mode.
757///
758/// Strongly consistent: 1 RCU per 4KB, rounded up.
759/// Eventually consistent: 0.5 RCU per 4KB (half the strongly consistent rate).
760pub fn read_capacity_units_with_consistency(item_size_bytes: usize, consistent: bool) -> f64 {
761    let strongly = read_capacity_units(item_size_bytes);
762    if consistent { strongly } else { strongly / 2.0 }
763}
764
765/// Build a `ConsumedCapacity` for a simple table operation.
766pub fn consumed_capacity(
767    table_name: &str,
768    capacity_units: f64,
769    mode: &Option<String>,
770) -> Option<ConsumedCapacity> {
771    let mode = mode.as_deref().unwrap_or("NONE");
772    match mode {
773        "TOTAL" => Some(ConsumedCapacity {
774            table_name: table_name.to_string(),
775            capacity_units,
776            table: None,
777            global_secondary_indexes: None,
778            local_secondary_indexes: None,
779        }),
780        "INDEXES" => Some(ConsumedCapacity {
781            table_name: table_name.to_string(),
782            capacity_units,
783            table: Some(CapacityDetail { capacity_units }),
784            global_secondary_indexes: None,
785            local_secondary_indexes: None,
786        }),
787        _ => None,
788    }
789}
790
791/// Build a `ConsumedCapacity` with per-GSI breakdown for INDEXES mode.
792pub fn consumed_capacity_with_indexes(
793    table_name: &str,
794    table_units: f64,
795    gsi_units: &HashMap<String, f64>,
796    mode: &Option<String>,
797) -> Option<ConsumedCapacity> {
798    consumed_capacity_with_secondary_indexes(
799        table_name,
800        table_units,
801        gsi_units,
802        &HashMap::new(),
803        mode,
804    )
805}
806
807/// Build a `ConsumedCapacity` with per-GSI and per-LSI breakdown for INDEXES mode.
808pub fn consumed_capacity_with_secondary_indexes(
809    table_name: &str,
810    table_units: f64,
811    gsi_units: &HashMap<String, f64>,
812    lsi_units: &HashMap<String, f64>,
813    mode: &Option<String>,
814) -> Option<ConsumedCapacity> {
815    let units_to_map = |units: &HashMap<String, f64>| -> Option<HashMap<String, CapacityDetail>> {
816        if units.is_empty() {
817            None
818        } else {
819            Some(
820                units
821                    .iter()
822                    .map(|(name, &u)| (name.clone(), CapacityDetail { capacity_units: u }))
823                    .collect(),
824            )
825        }
826    };
827
828    match mode.as_deref().unwrap_or("NONE") {
829        "INDEXES" => {
830            let gsi_total: f64 = gsi_units.values().sum();
831            let lsi_total: f64 = lsi_units.values().sum();
832            Some(ConsumedCapacity {
833                table_name: table_name.to_string(),
834                capacity_units: table_units + gsi_total + lsi_total,
835                table: Some(CapacityDetail {
836                    capacity_units: table_units,
837                }),
838                global_secondary_indexes: units_to_map(gsi_units),
839                local_secondary_indexes: units_to_map(lsi_units),
840            })
841        }
842        "TOTAL" => {
843            let gsi_total: f64 = gsi_units.values().sum();
844            let lsi_total: f64 = lsi_units.values().sum();
845            Some(ConsumedCapacity {
846                table_name: table_name.to_string(),
847                capacity_units: table_units + gsi_total + lsi_total,
848                table: None,
849                global_secondary_indexes: None,
850                local_secondary_indexes: None,
851            })
852        }
853        _ => None,
854    }
855}
856
857/// Key schema element — defines a key attribute.
858#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
859pub struct KeySchemaElement {
860    #[serde(rename = "AttributeName", alias = "attribute_name")]
861    pub attribute_name: String,
862    #[serde(rename = "KeyType", alias = "key_type")]
863    pub key_type: KeyType,
864}
865
866/// Key type: HASH (partition key) or RANGE (sort key).
867#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
868pub enum KeyType {
869    #[default]
870    HASH,
871    RANGE,
872}
873
874/// Attribute definition — declares an attribute's type.
875#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
876pub struct AttributeDefinition {
877    #[serde(rename = "AttributeName", alias = "attribute_name")]
878    pub attribute_name: String,
879    #[serde(rename = "AttributeType", alias = "attribute_type")]
880    pub attribute_type: ScalarAttributeType,
881}
882
883/// Scalar attribute types that can be used as keys.
884#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
885pub enum ScalarAttributeType {
886    #[default]
887    S,
888    N,
889    B,
890}
891
892/// GSI projection type.
893#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
894pub struct Projection {
895    #[serde(
896        rename = "ProjectionType",
897        alias = "projection_type",
898        default,
899        skip_serializing_if = "Option::is_none"
900    )]
901    pub projection_type: Option<ProjectionType>,
902    #[serde(
903        rename = "NonKeyAttributes",
904        alias = "non_key_attributes",
905        skip_serializing_if = "Option::is_none"
906    )]
907    pub non_key_attributes: Option<Vec<String>>,
908}
909
910/// Projection type enum.
911#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
912#[allow(non_camel_case_types)]
913pub enum ProjectionType {
914    #[default]
915    ALL,
916    KEYS_ONLY,
917    INCLUDE,
918}
919
920/// Global Secondary Index definition.
921#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
922pub struct GlobalSecondaryIndex {
923    #[serde(rename = "IndexName", alias = "index_name")]
924    pub index_name: String,
925    #[serde(rename = "KeySchema", alias = "key_schema")]
926    pub key_schema: Vec<KeySchemaElement>,
927    #[serde(rename = "Projection", alias = "projection")]
928    pub projection: Projection,
929    #[serde(
930        rename = "ProvisionedThroughput",
931        alias = "provisioned_throughput",
932        skip_serializing_if = "Option::is_none"
933    )]
934    pub provisioned_throughput: Option<ProvisionedThroughput>,
935}
936
937/// Local Secondary Index definition.
938#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
939pub struct LocalSecondaryIndex {
940    #[serde(rename = "IndexName", alias = "index_name")]
941    pub index_name: String,
942    #[serde(rename = "KeySchema", alias = "key_schema")]
943    pub key_schema: Vec<KeySchemaElement>,
944    #[serde(rename = "Projection", alias = "projection")]
945    pub projection: Projection,
946}
947
948/// Provisioned throughput settings (stored but not enforced).
949#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
950pub struct ProvisionedThroughput {
951    #[serde(rename = "ReadCapacityUnits", default)]
952    pub read_capacity_units: Option<i64>,
953    #[serde(rename = "WriteCapacityUnits", default)]
954    pub write_capacity_units: Option<i64>,
955}
956
957// ---------------------------------------------------------------------------
958// Type conversion: From<T> / TryFrom<T> for AttributeValue
959// ---------------------------------------------------------------------------
960
961/// Error returned when converting between `AttributeValue` and Rust types.
962#[derive(Debug, Clone, PartialEq)]
963pub struct ConversionError {
964    /// The expected DynamoDB or Rust type.
965    pub expected: &'static str,
966    /// The actual DynamoDB type encountered.
967    pub actual: &'static str,
968}
969
970impl fmt::Display for ConversionError {
971    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
972        write!(f, "expected {}, got {}", self.expected, self.actual)
973    }
974}
975
976impl std::error::Error for ConversionError {}
977
978// --- From<T> for AttributeValue: infallible conversions ---
979
980impl From<String> for AttributeValue {
981    fn from(value: String) -> Self {
982        AttributeValue::S(value)
983    }
984}
985
986impl From<&str> for AttributeValue {
987    fn from(value: &str) -> Self {
988        AttributeValue::S(value.to_string())
989    }
990}
991
992impl From<bool> for AttributeValue {
993    fn from(value: bool) -> Self {
994        AttributeValue::BOOL(value)
995    }
996}
997
998impl From<Vec<u8>> for AttributeValue {
999    fn from(value: Vec<u8>) -> Self {
1000        AttributeValue::B(value)
1001    }
1002}
1003
1004impl From<&[u8]> for AttributeValue {
1005    fn from(value: &[u8]) -> Self {
1006        AttributeValue::B(value.to_vec())
1007    }
1008}
1009
1010// Integer types — all finite, all fit in DynamoDB's number range.
1011macro_rules! impl_from_integer {
1012    ($($t:ty),+) => {
1013        $(
1014            impl From<$t> for AttributeValue {
1015                fn from(value: $t) -> Self {
1016                    AttributeValue::N(value.to_string())
1017                }
1018            }
1019        )+
1020    };
1021}
1022
1023impl_from_integer!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
1024
1025// Container types
1026impl From<HashMap<String, AttributeValue>> for AttributeValue {
1027    fn from(value: HashMap<String, AttributeValue>) -> Self {
1028        AttributeValue::M(value)
1029    }
1030}
1031
1032impl From<Vec<AttributeValue>> for AttributeValue {
1033    fn from(value: Vec<AttributeValue>) -> Self {
1034        AttributeValue::L(value)
1035    }
1036}
1037
1038impl From<HashSet<String>> for AttributeValue {
1039    fn from(value: HashSet<String>) -> Self {
1040        AttributeValue::SS(value.into_iter().collect())
1041    }
1042}
1043
1044impl From<BTreeSet<String>> for AttributeValue {
1045    fn from(value: BTreeSet<String>) -> Self {
1046        AttributeValue::SS(value.into_iter().collect())
1047    }
1048}
1049
1050// --- TryFrom<T> for AttributeValue: fallible conversions (floats) ---
1051
1052impl TryFrom<f64> for AttributeValue {
1053    type Error = ConversionError;
1054
1055    fn try_from(value: f64) -> std::result::Result<Self, Self::Error> {
1056        if value.is_finite() {
1057            Ok(AttributeValue::N(value.to_string()))
1058        } else {
1059            Err(ConversionError {
1060                expected: "finite f64",
1061                actual: "NaN or Infinity",
1062            })
1063        }
1064    }
1065}
1066
1067impl TryFrom<f32> for AttributeValue {
1068    type Error = ConversionError;
1069
1070    fn try_from(value: f32) -> std::result::Result<Self, Self::Error> {
1071        if value.is_finite() {
1072            Ok(AttributeValue::N(value.to_string()))
1073        } else {
1074            Err(ConversionError {
1075                expected: "finite f32",
1076                actual: "NaN or Infinity",
1077            })
1078        }
1079    }
1080}
1081
1082// --- TryFrom<AttributeValue> for T: extract Rust types from AV ---
1083
1084impl TryFrom<AttributeValue> for String {
1085    type Error = ConversionError;
1086
1087    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1088        match value {
1089            AttributeValue::S(s) => Ok(s),
1090            other => Err(ConversionError {
1091                expected: "S",
1092                actual: other.type_name(),
1093            }),
1094        }
1095    }
1096}
1097
1098impl TryFrom<AttributeValue> for bool {
1099    type Error = ConversionError;
1100
1101    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1102        match value {
1103            AttributeValue::BOOL(b) => Ok(b),
1104            other => Err(ConversionError {
1105                expected: "BOOL",
1106                actual: other.type_name(),
1107            }),
1108        }
1109    }
1110}
1111
1112impl TryFrom<AttributeValue> for Vec<u8> {
1113    type Error = ConversionError;
1114
1115    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1116        match value {
1117            AttributeValue::B(b) => Ok(b),
1118            other => Err(ConversionError {
1119                expected: "B",
1120                actual: other.type_name(),
1121            }),
1122        }
1123    }
1124}
1125
1126macro_rules! impl_try_from_av_integer {
1127    ($($t:ty),+) => {
1128        $(
1129            impl TryFrom<AttributeValue> for $t {
1130                type Error = ConversionError;
1131
1132                fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1133                    match value {
1134                        AttributeValue::N(n) => n.parse::<$t>().map_err(|_| ConversionError {
1135                            expected: stringify!($t),
1136                            actual: "N (parse failed)",
1137                        }),
1138                        other => Err(ConversionError {
1139                            expected: "N",
1140                            actual: other.type_name(),
1141                        }),
1142                    }
1143                }
1144            }
1145        )+
1146    };
1147}
1148
1149impl_try_from_av_integer!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
1150
1151impl TryFrom<AttributeValue> for f64 {
1152    type Error = ConversionError;
1153
1154    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1155        match value {
1156            AttributeValue::N(n) => n.parse::<f64>().map_err(|_| ConversionError {
1157                expected: "f64",
1158                actual: "N (parse failed)",
1159            }),
1160            other => Err(ConversionError {
1161                expected: "N",
1162                actual: other.type_name(),
1163            }),
1164        }
1165    }
1166}
1167
1168impl TryFrom<AttributeValue> for f32 {
1169    type Error = ConversionError;
1170
1171    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1172        match value {
1173            AttributeValue::N(n) => n.parse::<f32>().map_err(|_| ConversionError {
1174                expected: "f32",
1175                actual: "N (parse failed)",
1176            }),
1177            other => Err(ConversionError {
1178                expected: "N",
1179                actual: other.type_name(),
1180            }),
1181        }
1182    }
1183}
1184
1185impl TryFrom<AttributeValue> for HashMap<String, AttributeValue> {
1186    type Error = ConversionError;
1187
1188    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1189        match value {
1190            AttributeValue::M(m) => Ok(m),
1191            other => Err(ConversionError {
1192                expected: "M",
1193                actual: other.type_name(),
1194            }),
1195        }
1196    }
1197}
1198
1199impl TryFrom<AttributeValue> for Vec<AttributeValue> {
1200    type Error = ConversionError;
1201
1202    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1203        match value {
1204            AttributeValue::L(l) => Ok(l),
1205            other => Err(ConversionError {
1206                expected: "L",
1207                actual: other.type_name(),
1208            }),
1209        }
1210    }
1211}
1212
1213impl TryFrom<AttributeValue> for Vec<String> {
1214    type Error = ConversionError;
1215
1216    fn try_from(value: AttributeValue) -> std::result::Result<Self, ConversionError> {
1217        match value {
1218            AttributeValue::SS(ss) => Ok(ss),
1219            AttributeValue::L(l) => {
1220                // Lenient: extract S values from a list
1221                l.into_iter()
1222                    .map(|av| match av {
1223                        AttributeValue::S(s) => Ok(s),
1224                        other => Err(ConversionError {
1225                            expected: "S (within L)",
1226                            actual: other.type_name(),
1227                        }),
1228                    })
1229                    .collect()
1230            }
1231            other => Err(ConversionError {
1232                expected: "SS or L",
1233                actual: other.type_name(),
1234            }),
1235        }
1236    }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241    use super::*;
1242
1243    #[test]
1244    fn test_serialize_string() {
1245        let val = AttributeValue::S("hello".to_string());
1246        let json = serde_json::to_string(&val).unwrap();
1247        assert_eq!(json, r#"{"S":"hello"}"#);
1248    }
1249
1250    #[test]
1251    fn test_serialize_number() {
1252        let val = AttributeValue::N("42".to_string());
1253        let json = serde_json::to_string(&val).unwrap();
1254        assert_eq!(json, r#"{"N":"42"}"#);
1255    }
1256
1257    #[test]
1258    fn test_serialize_binary() {
1259        let val = AttributeValue::B(vec![1, 2, 3]);
1260        let json = serde_json::to_string(&val).unwrap();
1261        assert_eq!(json, r#"{"B":"AQID"}"#);
1262    }
1263
1264    #[test]
1265    fn test_serialize_bool() {
1266        let val = AttributeValue::BOOL(true);
1267        let json = serde_json::to_string(&val).unwrap();
1268        assert_eq!(json, r#"{"BOOL":true}"#);
1269    }
1270
1271    #[test]
1272    fn test_serialize_null() {
1273        let val = AttributeValue::NULL(true);
1274        let json = serde_json::to_string(&val).unwrap();
1275        assert_eq!(json, r#"{"NULL":true}"#);
1276    }
1277
1278    #[test]
1279    fn test_serialize_string_set() {
1280        let val = AttributeValue::SS(vec!["a".to_string(), "b".to_string()]);
1281        let json = serde_json::to_string(&val).unwrap();
1282        assert_eq!(json, r#"{"SS":["a","b"]}"#);
1283    }
1284
1285    #[test]
1286    fn test_serialize_list() {
1287        let val = AttributeValue::L(vec![
1288            AttributeValue::S("hello".to_string()),
1289            AttributeValue::N("42".to_string()),
1290        ]);
1291        let json = serde_json::to_string(&val).unwrap();
1292        assert_eq!(json, r#"{"L":[{"S":"hello"},{"N":"42"}]}"#);
1293    }
1294
1295    #[test]
1296    fn test_serialize_map() {
1297        let mut m = HashMap::new();
1298        m.insert("key".to_string(), AttributeValue::S("value".to_string()));
1299        let val = AttributeValue::M(m);
1300        let json = serde_json::to_string(&val).unwrap();
1301        assert_eq!(json, r#"{"M":{"key":{"S":"value"}}}"#);
1302    }
1303
1304    #[test]
1305    fn test_round_trip_all_types() {
1306        let values = vec![
1307            AttributeValue::S("hello".to_string()),
1308            AttributeValue::N("42.5".to_string()),
1309            AttributeValue::B(vec![0, 255, 128]),
1310            AttributeValue::BOOL(false),
1311            AttributeValue::NULL(true),
1312            AttributeValue::SS(vec!["x".to_string(), "y".to_string()]),
1313            AttributeValue::NS(vec!["1".to_string(), "2.5".to_string()]),
1314            AttributeValue::BS(vec![vec![1], vec![2, 3]]),
1315            AttributeValue::L(vec![
1316                AttributeValue::S("nested".to_string()),
1317                AttributeValue::N("99".to_string()),
1318            ]),
1319        ];
1320
1321        for val in values {
1322            let json = serde_json::to_string(&val).unwrap();
1323            let deserialized: AttributeValue = serde_json::from_str(&json).unwrap();
1324            assert_eq!(val, deserialized, "Round-trip failed for {json}");
1325        }
1326    }
1327
1328    #[test]
1329    fn test_size_string() {
1330        let val = AttributeValue::S("hello".to_string());
1331        assert_eq!(val.size(), 5);
1332    }
1333
1334    #[test]
1335    fn test_size_number() {
1336        // "42" has 2 significant digits → (2/2) + 1 = 2
1337        let val = AttributeValue::N("42".to_string());
1338        assert_eq!(val.size(), 2);
1339    }
1340
1341    #[test]
1342    fn test_size_bool() {
1343        assert_eq!(AttributeValue::BOOL(true).size(), 1);
1344    }
1345
1346    #[test]
1347    fn test_size_null() {
1348        assert_eq!(AttributeValue::NULL(true).size(), 1);
1349    }
1350
1351    #[test]
1352    fn test_key_string_s() {
1353        let val = AttributeValue::S("hello".to_string());
1354        assert_eq!(val.to_key_string(), Some("S:hello".to_string()));
1355    }
1356
1357    #[test]
1358    fn test_key_string_n() {
1359        let val = AttributeValue::N("42".to_string());
1360        let key = val.to_key_string().unwrap();
1361        assert!(key.starts_with("N:"));
1362    }
1363
1364    #[test]
1365    fn test_key_string_b() {
1366        let val = AttributeValue::B(vec![0xff, 0x00, 0xab]);
1367        assert_eq!(val.to_key_string(), Some("B:ff00ab".to_string()));
1368    }
1369
1370    #[test]
1371    fn test_key_string_non_key_type_returns_none() {
1372        assert_eq!(AttributeValue::BOOL(true).to_key_string(), None);
1373        assert_eq!(AttributeValue::L(vec![]).to_key_string(), None);
1374    }
1375
1376    // Number sort key ordering tests
1377    #[test]
1378    fn test_number_sort_ordering() {
1379        let numbers = vec![
1380            "-1000", "-100", "-10", "-1", "-0.5", "-0.001", "0", "0.001", "0.5", "1", "10", "100",
1381            "1000",
1382        ];
1383        let encoded: Vec<String> = numbers
1384            .iter()
1385            .map(|n| normalize_number_for_sort(n))
1386            .collect();
1387
1388        for i in 0..encoded.len() - 1 {
1389            assert!(
1390                encoded[i] < encoded[i + 1],
1391                "Sort order broken: {} ({}) should be < {} ({})",
1392                numbers[i],
1393                encoded[i],
1394                numbers[i + 1],
1395                encoded[i + 1]
1396            );
1397        }
1398    }
1399
1400    #[test]
1401    fn test_number_sort_zero_variants() {
1402        let z1 = normalize_number_for_sort("0");
1403        let z2 = normalize_number_for_sort("-0");
1404        let z3 = normalize_number_for_sort("0.0");
1405        assert_eq!(z1, z2);
1406        assert_eq!(z2, z3);
1407    }
1408
1409    #[test]
1410    fn test_number_sort_decimals() {
1411        let a = normalize_number_for_sort("1.5");
1412        let b = normalize_number_for_sort("2.5");
1413        assert!(a < b);
1414
1415        let c = normalize_number_for_sort("0.001");
1416        let d = normalize_number_for_sort("0.01");
1417        assert!(c < d);
1418    }
1419
1420    #[test]
1421    fn test_number_sort_scientific() {
1422        let a = normalize_number_for_sort("1e10");
1423        let b = normalize_number_for_sort("1e11");
1424        assert!(a < b);
1425
1426        let c = normalize_number_for_sort("-1e11");
1427        let d = normalize_number_for_sort("-1e10");
1428        assert!(c < d);
1429    }
1430
1431    #[test]
1432    fn test_type_name() {
1433        assert_eq!(AttributeValue::S("".to_string()).type_name(), "S");
1434        assert_eq!(AttributeValue::N("0".to_string()).type_name(), "N");
1435        assert_eq!(AttributeValue::B(vec![]).type_name(), "B");
1436        assert_eq!(AttributeValue::BOOL(true).type_name(), "BOOL");
1437        assert_eq!(AttributeValue::NULL(true).type_name(), "NULL");
1438        assert_eq!(AttributeValue::SS(vec![]).type_name(), "SS");
1439        assert_eq!(AttributeValue::NS(vec![]).type_name(), "NS");
1440        assert_eq!(AttributeValue::BS(vec![]).type_name(), "BS");
1441        assert_eq!(AttributeValue::L(vec![]).type_name(), "L");
1442        assert_eq!(AttributeValue::M(HashMap::new()).type_name(), "M");
1443    }
1444}