Skip to main content

aws_smithy_schema/schema/document/
deserializer.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! [`DocumentShapeDeserializer`] — a [`ShapeDeserializer`]
7//! implementation that walks an [`aws_smithy_types::Document`] tree.
8//!
9//! Generated `deserialize` methods on Smithy shapes call the
10//! [`ShapeDeserializer`] interface to drive structure / list / map
11//! consumer dispatch and read scalar leaves. Pointing such generated
12//! code at this deserializer reifies a [`Document`] tree as the
13//! corresponding typed shape — the inverse of [`DocumentShapeSerializer`].
14//!
15//! The deserializer holds a borrow of the source [`Document`] so reads
16//! are zero-copy where possible (e.g. [`String`] reads still clone the
17//! payload, but list/map navigation does not allocate).
18//!
19//! # Member-name resolution
20//!
21//! For struct reads, member dispatch uses the document's keys (not the
22//! schema's member list) so that documents created from a typed shape
23//! by [`DocumentShapeSerializer`] round-trip — those use Smithy member
24//! names. Wire-name resolution for `@jsonName` / `@xmlName`-style
25//! renames is the responsibility of the protocol's deserializer
26//! (`JsonDeserializer`, etc.) — not this generic Document walker. The
27//! `JsonFieldMapper` machinery in `aws-smithy-json` performs that
28//! mapping during the codec stage; this deserializer simply matches
29//! Smithy member names against document keys.
30//!
31//! Members present in the schema but absent from the document are not
32//! reported: generated builders default unset optional members to
33//! `None` and required-member enforcement is the builder's
34//! responsibility, not the deserializer's.
35//!
36//! [`DocumentShapeSerializer`]: super::DocumentShapeSerializer
37
38use std::str::FromStr;
39use std::sync::Arc;
40
41use aws_smithy_types::date_time::Format;
42use aws_smithy_types::{
43    BigDecimal, BigInteger, Blob, DateTime, Document, DocumentSettings, Number,
44};
45
46use crate::serde::{capped_container_size, SerdeError, ShapeDeserializer};
47use crate::Schema;
48
49/// Walks a [`Document`] tree via the [`ShapeDeserializer`] interface.
50///
51/// See the module-level documentation for an overview.
52///
53/// # Example
54///
55/// Use [`DiscriminatedDocument::as_shape`](crate::document::DiscriminatedDocumentExt::as_shape)
56/// for the common case of consuming a [`DiscriminatedDocument`](aws_smithy_types::DiscriminatedDocument)
57/// via a generated `deserialize` method:
58///
59/// ```ignore
60/// use aws_smithy_schema::document::DiscriminatedDocumentExt;
61///
62/// let person: Person = doc.as_shape(|deser| Person::deserialize(deser))?;
63/// ```
64///
65/// Direct construction is useful when consuming a sub-document outside
66/// the standard entry point:
67///
68/// ```ignore
69/// use aws_smithy_schema::document::DocumentShapeDeserializer;
70/// use aws_smithy_schema::serde::ShapeDeserializer;
71///
72/// let mut deser = DocumentShapeDeserializer::new(&doc);
73/// let s = deser.read_string(&aws_smithy_schema::prelude::STRING)?;
74/// ```
75#[derive(Debug)]
76pub struct DocumentShapeDeserializer<'a> {
77    /// The document this deserializer is currently positioned at.
78    cursor: &'a Document,
79    /// Optional codec settings used for format-aware coercion when
80    /// the cursor doesn't match a native variant. For example: a
81    /// document parsed from JSON wire bytes encodes blobs as strings
82    /// (base64) and may encode timestamps as strings or numbers; with
83    /// settings attached, [`Self::read_blob`] / [`Self::read_timestamp`]
84    /// fall back through [`DocumentSettings::coerce_string_to_blob`] /
85    /// [`DocumentSettings::coerce_string_to_timestamp`] /
86    /// [`DocumentSettings::coerce_number_to_timestamp`] when the
87    /// native variant isn't present.
88    ///
89    /// `None` for serialize-side documents (e.g. those produced by
90    /// [`super::DocumentShapeSerializer`]) that always carry native
91    /// variants — no coercion required.
92    settings: Option<Arc<dyn DocumentSettings>>,
93}
94
95impl<'a> DocumentShapeDeserializer<'a> {
96    /// Creates a deserializer positioned at the given document.
97    ///
98    /// The lifetime parameter is the borrow lifetime of `document` —
99    /// the cursor is just a `&Document`. [`Document`] itself has no
100    /// lifetime parameter (it is fully owned), so this `'a` is purely
101    /// the per-call borrow lifetime.
102    ///
103    /// No format-aware coercion is performed; this deserializer is
104    /// variant-only. For coercion of wire-encoded blobs and timestamps
105    /// (e.g. JSON's base64-string blobs, epoch-seconds-number
106    /// timestamps), use [`Self::new_with_settings`].
107    pub fn new(document: &'a Document) -> Self {
108        Self {
109            cursor: document,
110            settings: None,
111        }
112    }
113
114    /// Creates a deserializer positioned at the given document with
115    /// codec settings attached.
116    ///
117    /// With settings present, [`Self::read_blob`] and
118    /// [`Self::read_timestamp`] consult the settings to coerce
119    /// non-native variants — JSON wire bytes encode blobs as
120    /// base64 strings and may encode timestamps as strings or
121    /// numbers, so a document parsed from JSON via
122    /// [`crate::codec::Codec::create_deserializer`] needs settings
123    /// for those round-trips to succeed.
124    pub fn new_with_settings(
125        document: &'a Document,
126        settings: Option<Arc<dyn DocumentSettings>>,
127    ) -> Self {
128        Self {
129            cursor: document,
130            settings,
131        }
132    }
133}
134
135/// Interprets a [`Number`] as epoch seconds, retaining fractional
136/// seconds when present.
137///
138/// The deterministic default used by [`read_timestamp`](DocumentShapeDeserializer::read_timestamp)
139/// when no protocol settings are attached; the inverse of what
140/// [`DocumentShapeSerializer`](super::DocumentShapeSerializer) writes
141/// for a `timestamp` shape.
142fn number_to_timestamp(n: &Number) -> Result<DateTime, SerdeError> {
143    match n {
144        Number::PosInt(v) => i64::try_from(*v)
145            .map(DateTime::from_secs)
146            .map_err(|_| SerdeError::custom(format!("epoch seconds {v} out of range"))),
147        Number::NegInt(v) => Ok(DateTime::from_secs(*v)),
148        Number::Float(v) => {
149            if !v.is_finite() {
150                return Err(SerdeError::custom(format!(
151                    "epoch seconds {v} is not finite"
152                )));
153            }
154            Ok(DateTime::from_secs_f64(*v))
155        }
156    }
157}
158
159/// Builds a `TypeMismatch` error message for a read that expected one
160/// kind of value and found another.
161fn type_mismatch(expected: &str, found: &Document) -> SerdeError {
162    SerdeError::type_mismatch(format!(
163        "expected {expected} document, got {}",
164        kind_name(found)
165    ))
166}
167
168/// Human-readable name for a [`Document`] variant, used in error
169/// messages for type-mismatch diagnostics.
170fn kind_name(d: &Document) -> &'static str {
171    match d {
172        Document::Null => "null",
173        Document::Bool(_) => "boolean",
174        Document::Number(_) => "number",
175        Document::String(_) => "string",
176        Document::Array(_) => "list",
177        Document::Object(_) => "map",
178    }
179}
180
181/// Resolves a wire-level map key to a member of `schema`, matching
182/// against [`Schema::member_name`] directly.
183///
184/// Wire-name resolution for `@jsonName` / `@xmlName`-style renames is
185/// the responsibility of the protocol's deserializer (which has access
186/// to the codec settings); this generic Document walker matches Smithy
187/// member names only. Documents produced by [`DocumentShapeSerializer`](super::DocumentShapeSerializer)
188/// always use Smithy member names, so the round-trip is exact.
189fn resolve_member<'s>(schema: &'s Schema<'s>, wire_name: &str) -> Option<&'s Schema<'s>> {
190    let idx = schema
191        .members()
192        .iter()
193        .position(|m| m.member_name() == Some(wire_name))?;
194    schema.member_schema_by_index(idx)
195}
196
197// -- Numeric coercion ------------------------------------------------
198//
199// [`Document`] carries exactly the six JSON-shaped variants published in
200// `aws-smithy-types`, and nothing more: no numeric-coercion accessors,
201// no arbitrary-precision accessors. Every Smithy-numeric coercion
202// therefore lives here, on the schema side, where the schema has already
203// established which Smithy type the caller is asking for.
204//
205// The signed-integer coercions accept the bounded integer variants of
206// [`Number`] (`PosInt` / `NegInt`) and report overflow. They also accept a
207// `Number::Float` whose value is an integer that fits the target (`42.0`
208// reads as `42`), because `DiscriminatedDocument::shape_type` reports such
209// a value as `integer` or `long` per the SEP's ambiguous-number rule, and a
210// reported type must be readable. A float with a fractional part, or a
211// non-finite one, is rejected as lossy rather than truncated: the SEP's
212// "ignore loss of precision" wording would allow truncation, but legacy JSON
213// deserialization rejects it, and silently dropping digits is the riskier
214// choice. The reverse direction is lossless and is allowed —
215// [`coerce_float`] / [`coerce_double`] take integer sources.
216//
217// `Document` has no arbitrary-precision variant either, so the
218// schema-driven legacy representation carries `bigInteger` /
219// `bigDecimal` as [`Document::String`]. [`coerce_big_integer`] /
220// [`coerce_big_decimal`] accept a string source for exactly that
221// reason: they are only reached once a schema has established the
222// target shape is arbitrary-precision.
223
224/// Coerces a numeric [`Document`] to a signed integer target type.
225///
226/// Used by [`ShapeDeserializer::read_byte`] / `read_short` /
227/// `read_integer` / `read_long`.
228///
229/// The generic `T: TryFrom<i64> + TryFrom<u64>` bounds route
230/// `Number::PosInt(u64)` and `Number::NegInt(i64)` through the standard
231/// library's range-checked narrowing impls; no bespoke arithmetic.
232///
233/// A `Number::Float` is accepted when it is an integer within range (see
234/// [`float_as_i64`]); a fractional or non-finite float produces
235/// [`SerdeError::NumericCoercionLossy`]. Out-of-range sources produce
236/// [`SerdeError::NumericCoercionOverflow`].
237fn coerce_signed<T>(doc: &Document, name: &str) -> Result<T, SerdeError>
238where
239    T: TryFrom<i64> + TryFrom<u64>,
240{
241    match doc {
242        Document::Number(Number::PosInt(v)) => {
243            T::try_from(*v).map_err(|_| SerdeError::numeric_coercion_overflow(name, v.to_string()))
244        }
245        Document::Number(Number::NegInt(v)) => {
246            T::try_from(*v).map_err(|_| SerdeError::numeric_coercion_overflow(name, v.to_string()))
247        }
248        Document::Number(Number::Float(v)) => {
249            if !is_integral(*v) {
250                return Err(SerdeError::numeric_coercion_lossy(name, v.to_string()));
251            }
252            float_as_i64(*v)
253                .and_then(|i| T::try_from(i).ok())
254                .ok_or_else(|| SerdeError::numeric_coercion_overflow(name, v.to_string()))
255        }
256        other => Err(type_mismatch(name, other)),
257    }
258}
259
260/// Whether `v` is finite and has no fractional part.
261fn is_integral(v: f64) -> bool {
262    v.is_finite() && v.fract() == 0.0
263}
264
265/// The value of `v` as an `i64`, when `v` is an integer within `i64`'s range.
266///
267/// The upper bound is exclusive and written as 2⁶³ because `i64::MAX as f64` rounds *up* to
268/// 2⁶³, which is one past `i64::MAX`. `i64::MIN` (-2⁶³) is exactly representable, so the lower
269/// bound is inclusive. Shared with `DiscriminatedDocument::shape_type` so the reported type and
270/// the reader agree.
271pub(crate) fn float_as_i64(v: f64) -> Option<i64> {
272    const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
273    (is_integral(v) && (-TWO_POW_63..TWO_POW_63).contains(&v)).then_some(v as i64)
274}
275
276/// Coerces a numeric [`Document`] to `f64`.
277///
278/// Accepts every numeric variant: integer sources widen losslessly.
279fn coerce_double(doc: &Document) -> Result<f64, SerdeError> {
280    match doc {
281        Document::Number(Number::PosInt(v)) => Ok(*v as f64),
282        Document::Number(Number::NegInt(v)) => Ok(*v as f64),
283        Document::Number(Number::Float(f)) => Ok(*f),
284        other => Err(type_mismatch("double", other)),
285    }
286}
287
288/// Coerces a numeric [`Document`] to `f32`.
289///
290/// `Number::Float` is already `f64` and is narrowed by `as` cast
291/// (precision loss accepted per SEP).
292fn coerce_float(doc: &Document) -> Result<f32, SerdeError> {
293    Ok(coerce_double(doc)? as f32)
294}
295
296/// Coerces a [`Document`] to a [`BigInteger`].
297///
298/// Coerces from `Number::PosInt` / `Number::NegInt` by string-formatting
299/// (always lossless). A `Number::Float` is accepted when its value is an
300/// integer, at any magnitude, since every finite integral `f64` has an exact
301/// decimal form; a fractional or non-finite float produces
302/// [`SerdeError::NumericCoercionLossy`].
303///
304/// A [`Document::String`] source is accepted because the schema-driven
305/// legacy representation of an arbitrary-precision shape is its numeric
306/// text. A string holding a decimal point or exponent is truncated
307/// toward zero, expanding scientific notation so the integer magnitude
308/// is preserved.
309fn coerce_big_integer(doc: &Document) -> Result<BigInteger, SerdeError> {
310    match doc {
311        Document::Number(Number::PosInt(v)) => parse_big_integer(&v.to_string()),
312        Document::Number(Number::NegInt(v)) => parse_big_integer(&v.to_string()),
313        Document::Number(Number::Float(v)) => {
314            if !is_integral(*v) {
315                return Err(SerdeError::numeric_coercion_lossy(
316                    "bigInteger",
317                    v.to_string(),
318                ));
319            }
320            // `{:.0}` prints every digit of an integral `f64` (no exponent); `0.0`
321            // keeps `-0.0` from printing as "-0".
322            parse_big_integer(&format!("{:.0}", v + 0.0))
323        }
324        Document::String(s) => {
325            if let Ok(bi) = BigInteger::from_str(s) {
326                return Ok(bi);
327            }
328            // Not integral text — try the decimal grammar and truncate
329            // toward zero, expanding any scientific-notation exponent so
330            // the magnitude is preserved. A naive split at the first
331            // '.' / 'e' / 'E' produces a silent wrong value (e.g.
332            // "1.23e10" -> "1"); dropping only the fractional digits is
333            // the SEP's "ignore loss of precision" rule.
334            let bd = BigDecimal::from_str(s).map_err(|e| invalid_input("bigInteger", s, &e))?;
335            // `BigDecimal::from_str` guarantees the decimal grammar, which
336            // is exactly what the magnitude algorithm needs. Re-check it
337            // here anyway: `big_decimal_to_integer_string` only
338            // `debug_assert!`s the precondition, so in release builds a
339            // future relaxation of `FromStr` would silently produce a
340            // wrong value rather than an error.
341            if !is_unambiguous_decimal(bd.as_ref()) {
342                return Err(SerdeError::invalid_input(format!(
343                    "cannot coerce {:?} to bigInteger: not a decimal number",
344                    bd.as_ref()
345                )));
346            }
347            let int_part = big_decimal_to_integer_string(&bd).ok_or_else(|| {
348                SerdeError::custom(format!(
349                    "cannot coerce bigDecimal {} to bigInteger: integer magnitude too large",
350                    bd.as_ref()
351                ))
352            })?;
353            parse_big_integer(&int_part)
354        }
355        other => Err(type_mismatch("bigInteger", other)),
356    }
357}
358
359/// Coerces a [`Document`] to a [`BigDecimal`].
360///
361/// Coerces from [`Number`] by string-formatting (`Number::Float`'s
362/// `to_string` produces a `BigDecimal`-parseable form). A
363/// [`Document::String`] source is accepted for the same reason as in
364/// [`coerce_big_integer`].
365fn coerce_big_decimal(doc: &Document) -> Result<BigDecimal, SerdeError> {
366    match doc {
367        Document::String(s) => parse_big_decimal(s),
368        Document::Number(Number::PosInt(v)) => parse_big_decimal(&v.to_string()),
369        Document::Number(Number::NegInt(v)) => parse_big_decimal(&v.to_string()),
370        Document::Number(Number::Float(f)) => {
371            if !f.is_finite() {
372                return Err(SerdeError::custom(format!(
373                    "cannot coerce non-finite float {f} to bigDecimal"
374                )));
375            }
376            parse_big_decimal(&f.to_string())
377        }
378        other => Err(type_mismatch("bigDecimal", other)),
379    }
380}
381
382/// Parses integral text into a [`BigInteger`], reporting malformed input
383/// as [`SerdeError::InvalidInput`].
384fn parse_big_integer(s: &str) -> Result<BigInteger, SerdeError> {
385    BigInteger::from_str(s).map_err(|e| invalid_input("bigInteger", s, &e))
386}
387
388/// Parses decimal text into a [`BigDecimal`], reporting malformed input
389/// as [`SerdeError::InvalidInput`].
390fn parse_big_decimal(s: &str) -> Result<BigDecimal, SerdeError> {
391    BigDecimal::from_str(s).map_err(|e| invalid_input("bigDecimal", s, &e))
392}
393
394/// Builds an `InvalidInput` error for arbitrary-precision parse
395/// failures.
396fn invalid_input(target: &str, value: &str, err: &dyn std::fmt::Display) -> SerdeError {
397    SerdeError::invalid_input(format!("cannot parse {value:?} as {target}: {err}"))
398}
399
400/// `true` iff `s` is non-empty and consists entirely of ASCII digits.
401fn is_ascii_digits(s: &str) -> bool {
402    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
403}
404
405/// `true` iff `text` is a decimal number [`big_decimal_to_integer_string`]
406/// can read unambiguously:
407/// `('+' | '-')? (digits ('.' digits*)? | '.' digits+)`
408/// `(('e' | 'E') ('+' | '-')? digits+)?`.
409///
410/// This is the same structural grammar [`BigDecimal`]'s `FromStr` enforces,
411/// including leading signs and omitted integer or fractional digit runs. It is
412/// kept as an explicit precondition check because [`big_decimal_to_integer_string`]
413/// would return a silently wrong value rather than an error on text with no
414/// single numeric reading (`"1.2.3"` would truncate to `"1"`), and its own
415/// guard is a `debug_assert!` that disappears in release builds.
416///
417/// Deliberately *permissive* about leading zeros, matching `FromStr`:
418/// `"00123"` has one reading, and the algorithm normalizes it to `"123"`.
419/// (The stricter RFC 8259 check, which does forbid leading zeros, belongs
420/// on the wire boundary in the JSON codec's serializer — there the
421/// constraint is the output format, not readability.)
422fn is_unambiguous_decimal(text: &str) -> bool {
423    let rest = text.strip_prefix(['-', '+']).unwrap_or(text);
424
425    let (mantissa, exponent) = match rest.split_once(['e', 'E']) {
426        Some((mantissa, exponent)) => (mantissa, Some(exponent)),
427        None => (rest, None),
428    };
429
430    let mantissa_ok = match mantissa.split_once('.') {
431        Some((int_part, frac_part)) => {
432            (!int_part.is_empty() || !frac_part.is_empty())
433                && int_part.bytes().all(|b| b.is_ascii_digit())
434                && frac_part.bytes().all(|b| b.is_ascii_digit())
435        }
436        None => is_ascii_digits(mantissa),
437    };
438
439    let exponent_ok = match exponent {
440        None => true,
441        Some(exponent) => is_ascii_digits(exponent.strip_prefix(['+', '-']).unwrap_or(exponent)),
442    };
443
444    mantissa_ok && exponent_ok
445}
446
447/// Upper bound on the length of the integer string
448/// [`big_decimal_to_integer_string`] will materialize. A
449/// scientific-notation exponent can request an arbitrarily long run of
450/// trailing zeros; this cap stops a pathological exponent (e.g.
451/// `1e1000000000`) from triggering a huge allocation. ~1M digits is far
452/// beyond any practical value.
453const MAX_INTEGER_DIGITS: usize = 1 << 20;
454
455/// Returns `bd` truncated toward zero as a plain integer string
456/// (`-?[0-9]+`), or `None` if the integer magnitude is too large to
457/// materialize (see [`MAX_INTEGER_DIGITS`]).
458///
459/// Fractional digits are dropped — per the SEP, numeric coercion ignores
460/// loss of precision — and any scientific-notation exponent is expanded
461/// so the integer magnitude is preserved. For example `1.23e10` yields
462/// `"12300000000"`, `1.23e1` yields `"12"`, and `5e-3` yields `"0"`.
463///
464/// # Precondition
465///
466/// `bd` must satisfy [`is_unambiguous_decimal`]. That is implied by
467/// `BigDecimal::from_str` today, which enforces the same grammar, but this
468/// function returns a value rather than an error when given text with no
469/// single numeric reading — so the precondition is checked explicitly by
470/// the caller and only `debug_assert!`ed here.
471fn big_decimal_to_integer_string(bd: &BigDecimal) -> Option<String> {
472    debug_assert!(
473        is_unambiguous_decimal(bd.as_ref()),
474        "big_decimal_to_integer_string requires decimal text, got {:?}",
475        bd.as_ref()
476    );
477    let text = bd.as_ref();
478    let (negative, rest) = match text.strip_prefix('-') {
479        Some(rest) => (true, rest),
480        None => (false, text.strip_prefix('+').unwrap_or(text)),
481    };
482
483    let (mantissa, exp) = match rest.split_once(['e', 'E']) {
484        Some((mantissa, exp_str)) => match exp_str.parse::<i64>() {
485            Ok(exp) => (mantissa, exp),
486            // Exponent doesn't fit in i64: a huge positive exponent is
487            // too large to materialize. `BigDecimal::from_str`'s scale bound
488            // prevents an i64-underflowing negative exponent from reaching
489            // this branch today; retain the negative case as defense in depth.
490            Err(_) => {
491                return if exp_str.starts_with('-') {
492                    Some("0".to_string())
493                } else {
494                    None
495                }
496            }
497        },
498        None => (rest, 0),
499    };
500
501    let (int_digits, frac_digits) = match mantissa.split_once('.') {
502        Some((int_digits, frac_digits)) => (int_digits, frac_digits),
503        None => (mantissa, ""),
504    };
505
506    // Position of the decimal point from the left of the combined
507    // `int_digits ++ frac_digits` run, shifted right by the exponent.
508    // `saturating_add` keeps a near-`i64::MAX` exponent from overflowing
509    // (it then trips the size cap below).
510    let point = (int_digits.len() as i64).saturating_add(exp);
511
512    let int_part = if point <= 0 {
513        // The whole value is fractional.
514        "0".to_string()
515    } else {
516        let point = point as usize;
517        let mut digits = String::with_capacity(int_digits.len() + frac_digits.len());
518        digits.push_str(int_digits);
519        digits.push_str(frac_digits);
520
521        if point >= digits.len() {
522            // Decimal point at or beyond the last digit: pad with zeros.
523            if point > MAX_INTEGER_DIGITS {
524                return None;
525            }
526            digits.push_str(&"0".repeat(point - digits.len()));
527            digits
528        } else {
529            // Decimal point falls within the digit run; drop the rest.
530            digits.truncate(point);
531            digits
532        }
533    };
534
535    // Normalize leading zeros, keeping at least one digit.
536    let normalized = match int_part.trim_start_matches('0') {
537        "" => "0",
538        trimmed => trimmed,
539    };
540
541    Some(if negative && normalized != "0" {
542        format!("-{normalized}")
543    } else {
544        normalized.to_string()
545    })
546}
547
548impl<'a> ShapeDeserializer for DocumentShapeDeserializer<'a> {
549    fn read_struct(
550        &mut self,
551        schema: &Schema<'_>,
552        consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
553    ) -> Result<(), SerdeError> {
554        let map = self
555            .cursor
556            .as_object()
557            .ok_or_else(|| type_mismatch("struct (map)", self.cursor))?;
558        for (key, value) in map {
559            let Some(member_schema) = resolve_member(schema, key) else {
560                // Unknown member — silently ignore. Matches the
561                // tolerant "ignore unknown fields" behavior of the
562                // JSON deserializer.
563                continue;
564            };
565            let mut sub = Self::new_with_settings(value, self.settings.clone());
566            consumer(member_schema, &mut sub)?;
567        }
568        Ok(())
569    }
570
571    fn read_list(
572        &mut self,
573        _schema: &Schema<'_>,
574        consumer: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
575    ) -> Result<(), SerdeError> {
576        let items = self
577            .cursor
578            .as_array()
579            .ok_or_else(|| type_mismatch("list", self.cursor))?;
580        for item in items {
581            let mut sub = Self::new_with_settings(item, self.settings.clone());
582            consumer(&mut sub)?;
583        }
584        Ok(())
585    }
586
587    fn read_map(
588        &mut self,
589        _schema: &Schema<'_>,
590        consumer: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
591    ) -> Result<(), SerdeError> {
592        let entries = self
593            .cursor
594            .as_object()
595            .ok_or_else(|| type_mismatch("map", self.cursor))?;
596        for (key, value) in entries {
597            let mut sub = Self::new_with_settings(value, self.settings.clone());
598            consumer(key.clone(), &mut sub)?;
599        }
600        Ok(())
601    }
602
603    fn read_boolean(&mut self, _schema: &Schema<'_>) -> Result<bool, SerdeError> {
604        self.cursor
605            .as_bool()
606            .ok_or_else(|| type_mismatch("boolean", self.cursor))
607    }
608
609    fn read_byte(&mut self, _schema: &Schema<'_>) -> Result<i8, SerdeError> {
610        coerce_signed::<i8>(self.cursor, "byte")
611    }
612
613    fn read_short(&mut self, _schema: &Schema<'_>) -> Result<i16, SerdeError> {
614        coerce_signed::<i16>(self.cursor, "short")
615    }
616
617    fn read_integer(&mut self, _schema: &Schema<'_>) -> Result<i32, SerdeError> {
618        coerce_signed::<i32>(self.cursor, "integer")
619    }
620
621    fn read_long(&mut self, _schema: &Schema<'_>) -> Result<i64, SerdeError> {
622        coerce_signed::<i64>(self.cursor, "long")
623    }
624
625    fn read_float(&mut self, _schema: &Schema<'_>) -> Result<f32, SerdeError> {
626        coerce_float(self.cursor)
627    }
628
629    fn read_double(&mut self, _schema: &Schema<'_>) -> Result<f64, SerdeError> {
630        coerce_double(self.cursor)
631    }
632
633    fn read_big_integer(&mut self, _schema: &Schema<'_>) -> Result<BigInteger, SerdeError> {
634        // `Document` has no arbitrary-precision variant: `coerce_big_integer`
635        // widens the numeric variants and reverses the legacy string
636        // representation that `DocumentShapeSerializer` writes.
637        coerce_big_integer(self.cursor)
638    }
639
640    fn read_big_decimal(&mut self, _schema: &Schema<'_>) -> Result<BigDecimal, SerdeError> {
641        coerce_big_decimal(self.cursor)
642    }
643
644    fn read_string(&mut self, _schema: &Schema<'_>) -> Result<String, SerdeError> {
645        match self.cursor {
646            Document::String(s) => Ok(s.clone()),
647            other => Err(type_mismatch("string", other)),
648        }
649    }
650
651    fn read_blob(&mut self, _schema: &Schema<'_>) -> Result<Blob, SerdeError> {
652        // `Document` has no blob variant. The legacy representation of a
653        // `blob` shape is a base64-encoded string.
654        //
655        // With settings attached, the codec decides the decoding (e.g.
656        // the JSON codec's `coerce_string_to_blob`). With no settings —
657        // the case for a document built by `DocumentShapeSerializer`
658        // outside a protocol context — fall back to standard base64,
659        // which is exactly what the serializer wrote.
660        match (self.cursor, &self.settings) {
661            (Document::String(s), Some(settings)) => settings
662                .coerce_string_to_blob(s)
663                .map(Blob::new)
664                .map_err(SerdeError::from),
665            (Document::String(s), None) => aws_smithy_types::base64::decode(s)
666                .map(Blob::new)
667                .map_err(|e| {
668                    SerdeError::custom(format!("cannot base64-decode string as blob: {e}"))
669                }),
670            (other, _) => Err(type_mismatch("blob", other)),
671        }
672    }
673
674    fn read_timestamp(&mut self, _schema: &Schema<'_>) -> Result<DateTime, SerdeError> {
675        // `Document` has no timestamp variant. The legacy representation
676        // written by `DocumentShapeSerializer` is epoch seconds as a
677        // number; a document parsed off a JSON-family wire may instead
678        // hold a formatted string.
679        //
680        // With settings attached, the codec's `DocumentSettings` supplies
681        // the format and the right `coerce_*_to_timestamp` is dispatched
682        // by source variant. With no settings, fall back to the
683        // deterministic defaults: epoch seconds for a number, RFC-3339
684        // for a string.
685        match (self.cursor, &self.settings) {
686            (Document::Number(n), Some(settings)) => settings
687                .coerce_number_to_timestamp(n)
688                .map_err(SerdeError::from),
689            (Document::Number(n), None) => number_to_timestamp(n),
690            (Document::String(s), Some(settings)) => settings
691                .coerce_string_to_timestamp(s)
692                .map_err(SerdeError::from),
693            (Document::String(s), None) => DateTime::from_str(s, Format::DateTime).map_err(|e| {
694                SerdeError::custom(format!("cannot parse string as a date-time timestamp: {e}"))
695            }),
696            (other, _) => Err(type_mismatch("timestamp", other)),
697        }
698    }
699
700    fn read_document(&mut self, _schema: &Schema<'_>) -> Result<Document, SerdeError> {
701        Ok(self.cursor.clone())
702    }
703
704    fn is_null(&self) -> bool {
705        matches!(self.cursor, Document::Null)
706    }
707
708    /// Nothing to advance. The cursor points at an already-parsed node of an owned
709    /// document tree, and `read_struct` hands each member its own sub-deserializer, so
710    /// declining a value has no effect on the parent's iteration.
711    fn skip_value(&mut self) -> Result<(), SerdeError> {
712        Ok(())
713    }
714
715    fn container_size(&self) -> Option<usize> {
716        let raw = match self.cursor {
717            Document::Array(items) => items.len(),
718            Document::Object(entries) => entries.len(),
719            _ => return None,
720        };
721        Some(capped_container_size(raw))
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use std::collections::HashMap;
728
729    use super::*;
730    use crate::document::DocumentShapeSerializer;
731    use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
732    use crate::{prelude, shape_id, Schema, ShapeId, ShapeType};
733
734    use aws_smithy_types::Number;
735
736    // -- Test schemas ----------------------------------------------------
737
738    const PERSON_ID: ShapeId<'static> = shape_id!("smithy.example", "Person");
739    const PERSON_NAME_ID: ShapeId<'static> = shape_id!("smithy.example", "Person", "name");
740    const PERSON_AGE_ID: ShapeId<'static> = shape_id!("smithy.example", "Person", "age");
741
742    static PERSON_NAME_MEMBER: Schema<'static> =
743        Schema::new_member(PERSON_NAME_ID, ShapeType::String, "name", 0);
744    static PERSON_AGE_MEMBER: Schema<'static> =
745        Schema::new_member(PERSON_AGE_ID, ShapeType::Integer, "age", 1);
746    static PERSON_SCHEMA: Schema<'static> = Schema::new_struct(
747        PERSON_ID,
748        ShapeType::Structure,
749        &[&PERSON_NAME_MEMBER, &PERSON_AGE_MEMBER],
750    );
751
752    /// Test struct + builder pair to drive `read_struct` consumer
753    /// dispatch the same way generated code does.
754    #[derive(Debug, Default, PartialEq)]
755    struct Person {
756        name: Option<String>,
757        age: Option<i32>,
758    }
759
760    impl SerializableStruct for Person {
761        fn schema(&self) -> &Schema<'_> {
762            &PERSON_SCHEMA
763        }
764
765        fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
766            if let Some(n) = &self.name {
767                ser.write_string(&PERSON_NAME_MEMBER, n)?;
768            }
769            if let Some(a) = self.age {
770                ser.write_integer(&PERSON_AGE_MEMBER, a)?;
771            }
772            Ok(())
773        }
774    }
775
776    fn deserialize_person(deser: &mut dyn ShapeDeserializer) -> Result<Person, SerdeError> {
777        let mut out = Person::default();
778        deser.read_struct(&PERSON_SCHEMA, &mut |member, sub| {
779            match member.member_index() {
780                Some(0) => out.name = Some(sub.read_string(member)?),
781                Some(1) => out.age = Some(sub.read_integer(member)?),
782                _ => {}
783            }
784            Ok(())
785        })?;
786        Ok(out)
787    }
788
789    // -- Scalars ---------------------------------------------------------
790
791    #[test]
792    fn read_string_returns_value() {
793        let doc = Document::String("hello".to_string());
794        let mut deser = DocumentShapeDeserializer::new(&doc);
795        assert_eq!(deser.read_string(&prelude::STRING).unwrap(), "hello");
796    }
797
798    #[test]
799    fn read_string_on_non_string_errors() {
800        let doc = Document::Number(Number::PosInt(1));
801        let mut deser = DocumentShapeDeserializer::new(&doc);
802        let err = deser.read_string(&prelude::STRING).unwrap_err();
803        assert!(matches!(err, SerdeError::TypeMismatch { .. }));
804    }
805
806    #[test]
807    fn read_integer_with_coercion() {
808        // Integer narrowing follows SEP rules (precision loss ignored).
809        let doc = Document::Number(Number::PosInt(42));
810        let mut deser = DocumentShapeDeserializer::new(&doc);
811        assert_eq!(deser.read_integer(&prelude::INTEGER).unwrap(), 42);
812    }
813
814    #[test]
815    fn read_integer_overflow_errors() {
816        let doc = Document::Number(Number::PosInt(u64::MAX));
817        let mut deser = DocumentShapeDeserializer::new(&doc);
818        let err = deser.read_integer(&prelude::INTEGER).unwrap_err();
819        assert!(matches!(err, SerdeError::NumericCoercionOverflow { .. }));
820    }
821
822    #[test]
823    fn read_boolean_returns_value() {
824        let doc = Document::Bool(true);
825        let mut deser = DocumentShapeDeserializer::new(&doc);
826        assert!(deser.read_boolean(&prelude::BOOLEAN).unwrap());
827    }
828
829    #[test]
830    fn read_blob_decodes_base64_string_without_settings() {
831        // `Document` has no blob variant: the legacy representation is a
832        // base64 string, and with no settings attached the deserializer
833        // decodes it with standard base64 — exactly what
834        // `DocumentShapeSerializer::write_blob` wrote.
835        let doc = Document::String("YWJjZA==".to_string());
836        let mut deser = DocumentShapeDeserializer::new(&doc);
837        let blob = deser.read_blob(&prelude::BLOB).unwrap();
838        assert_eq!(blob.as_ref(), b"abcd");
839    }
840
841    #[test]
842    fn read_blob_on_malformed_base64_errors() {
843        let doc = Document::String("not base64!!!".to_string());
844        let mut deser = DocumentShapeDeserializer::new(&doc);
845        let err = deser.read_blob(&prelude::BLOB).unwrap_err();
846        let msg = format!("{err}");
847        assert!(msg.contains("base64"), "unexpected error message: {msg}");
848    }
849
850    #[test]
851    fn read_blob_on_non_string_errors() {
852        let doc = Document::Number(Number::PosInt(1));
853        let mut deser = DocumentShapeDeserializer::new(&doc);
854        let err = deser.read_blob(&prelude::BLOB).unwrap_err();
855        assert!(matches!(err, SerdeError::TypeMismatch { .. }));
856    }
857
858    #[test]
859    fn read_timestamp_defaults_to_epoch_seconds_without_settings() {
860        let doc = Document::Number(Number::PosInt(1234));
861        let mut deser = DocumentShapeDeserializer::new(&doc);
862        assert_eq!(
863            deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
864            DateTime::from_secs(1234)
865        );
866
867        let doc = Document::Number(Number::NegInt(-9));
868        let mut deser = DocumentShapeDeserializer::new(&doc);
869        assert_eq!(
870            deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
871            DateTime::from_secs(-9)
872        );
873    }
874
875    #[test]
876    fn read_timestamp_retains_fractional_seconds() {
877        let doc = Document::Number(Number::Float(1234.5));
878        let mut deser = DocumentShapeDeserializer::new(&doc);
879        assert_eq!(
880            deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
881            DateTime::from_secs_f64(1234.5)
882        );
883    }
884
885    #[test]
886    fn read_timestamp_parses_date_time_string_without_settings() {
887        let doc = Document::String("1970-01-01T00:00:00Z".to_string());
888        let mut deser = DocumentShapeDeserializer::new(&doc);
889        assert_eq!(
890            deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
891            DateTime::from_secs(0)
892        );
893    }
894
895    #[test]
896    fn read_timestamp_on_non_coercible_variant_errors() {
897        let doc = Document::Bool(true);
898        let mut deser = DocumentShapeDeserializer::new(&doc);
899        let err = deser.read_timestamp(&prelude::TIMESTAMP).unwrap_err();
900        assert!(matches!(err, SerdeError::TypeMismatch { .. }));
901    }
902
903    #[test]
904    fn read_big_integer_and_decimal_reverse_the_string_representation() {
905        // The legacy representation of an arbitrary-precision shape is
906        // its numeric text, so precision beyond f64's range survives.
907        let big = "12345678901234567890123456789";
908        let doc = Document::String(big.to_string());
909        let mut deser = DocumentShapeDeserializer::new(&doc);
910        assert_eq!(
911            deser
912                .read_big_integer(&prelude::BIG_INTEGER)
913                .unwrap()
914                .as_ref(),
915            big
916        );
917
918        let dec = "12345678901234567890123456789.25";
919        let doc = Document::String(dec.to_string());
920        let mut deser = DocumentShapeDeserializer::new(&doc);
921        assert_eq!(
922            deser
923                .read_big_decimal(&prelude::BIG_DECIMAL)
924                .unwrap()
925                .as_ref(),
926            dec
927        );
928    }
929
930    #[test]
931    fn is_null_for_null_document() {
932        let doc = Document::Null;
933        let deser = DocumentShapeDeserializer::new(&doc);
934        assert!(deser.is_null());
935    }
936
937    #[test]
938    fn is_null_false_for_non_null() {
939        let doc = Document::String("not-null".to_string());
940        let deser = DocumentShapeDeserializer::new(&doc);
941        assert!(!deser.is_null());
942    }
943
944    // -- Aggregates ------------------------------------------------------
945
946    #[test]
947    fn read_list_iterates_elements() {
948        let doc = Document::Array(vec![
949            Document::String("a".to_string()),
950            Document::String("b".to_string()),
951            Document::String("c".to_string()),
952        ]);
953        let mut deser = DocumentShapeDeserializer::new(&doc);
954        let mut collected = Vec::new();
955        deser
956            .read_list(&prelude::DOCUMENT, &mut |sub| {
957                collected.push(sub.read_string(&prelude::STRING)?);
958                Ok(())
959            })
960            .unwrap();
961        assert_eq!(collected, ["a", "b", "c"]);
962    }
963
964    #[test]
965    fn read_map_iterates_entries() {
966        let doc = Document::Object(HashMap::from([
967            ("k1".to_string(), Document::String("v1".to_string())),
968            ("k2".to_string(), Document::String("v2".to_string())),
969        ]));
970        let mut deser = DocumentShapeDeserializer::new(&doc);
971        let mut collected = HashMap::new();
972        deser
973            .read_map(&prelude::DOCUMENT, &mut |key, sub| {
974                collected.insert(key, sub.read_string(&prelude::STRING)?);
975                Ok(())
976            })
977            .unwrap();
978        assert_eq!(collected.get("k1").map(String::as_str), Some("v1"));
979        assert_eq!(collected.get("k2").map(String::as_str), Some("v2"));
980    }
981
982    #[test]
983    fn container_size_on_list() {
984        let doc = Document::Array(vec![Document::Null; 5]);
985        let deser = DocumentShapeDeserializer::new(&doc);
986        assert_eq!(deser.container_size(), Some(5));
987    }
988
989    #[test]
990    fn container_size_on_scalar_is_none() {
991        let doc = Document::String("foo".to_string());
992        let deser = DocumentShapeDeserializer::new(&doc);
993        assert!(deser.container_size().is_none());
994    }
995
996    // -- Struct round-trip ----------------------------------------------
997
998    #[test]
999    fn read_struct_with_consumer_dispatch() {
1000        let doc = Document::Object(HashMap::from([
1001            ("name".to_string(), Document::String("Alex".to_string())),
1002            ("age".to_string(), Document::Number(Number::PosInt(30))),
1003        ]));
1004        let mut deser = DocumentShapeDeserializer::new(&doc);
1005        let person = deserialize_person(&mut deser).unwrap();
1006        assert_eq!(
1007            person,
1008            Person {
1009                name: Some("Alex".into()),
1010                age: Some(30),
1011            }
1012        );
1013    }
1014
1015    #[test]
1016    fn read_struct_with_missing_optional_member() {
1017        // Document only has `name`; `age` is missing.
1018        let doc = Document::Object(HashMap::from([(
1019            "name".to_string(),
1020            Document::String("Sam".to_string()),
1021        )]));
1022        let mut deser = DocumentShapeDeserializer::new(&doc);
1023        let person = deserialize_person(&mut deser).unwrap();
1024        assert_eq!(
1025            person,
1026            Person {
1027                name: Some("Sam".into()),
1028                age: None,
1029            }
1030        );
1031    }
1032
1033    #[test]
1034    fn read_struct_ignores_unknown_members() {
1035        let doc = Document::Object(HashMap::from([
1036            ("name".to_string(), Document::String("Joe".to_string())),
1037            (
1038                "unknown_field".to_string(),
1039                Document::String("ignored".to_string()),
1040            ),
1041        ]));
1042        let mut deser = DocumentShapeDeserializer::new(&doc);
1043        let person = deserialize_person(&mut deser).unwrap();
1044        assert_eq!(person.name.as_deref(), Some("Joe"));
1045    }
1046
1047    #[test]
1048    fn read_struct_on_non_map_errors() {
1049        let doc = Document::String("not-a-struct".to_string());
1050        let mut deser = DocumentShapeDeserializer::new(&doc);
1051        let err = deserialize_person(&mut deser).unwrap_err();
1052        assert!(matches!(err, SerdeError::TypeMismatch { .. }));
1053    }
1054
1055    // -- Round-trip with the serializer ---------------------------------
1056
1057    #[test]
1058    fn round_trip_through_document() {
1059        let original = Person {
1060            name: Some("Iago".into()),
1061            age: Some(7),
1062        };
1063        // serialize → DiscriminatedDocument
1064        let mut ser = DocumentShapeSerializer::new();
1065        ser.write_struct(&PERSON_SCHEMA, &original).unwrap();
1066        let doc = ser.finish().unwrap();
1067        // deserialize the inner Document → typed
1068        let mut deser = DocumentShapeDeserializer::new(doc.document());
1069        let restored = deserialize_person(&mut deser).unwrap();
1070        assert_eq!(restored, original);
1071        // discriminator is preserved at the wrapper level
1072        assert_eq!(doc.discriminator(), Some("smithy.example#Person"));
1073    }
1074
1075    // -- Numeric coercion (moved off `Document`) -------------------------
1076    //
1077    // These exercise the private coercion helpers in this module through
1078    // the `ShapeDeserializer` surface. The logic used to live on
1079    // `Document` itself as additive `as_byte` / `coerce_big_integer`
1080    // accessors; `Document` is now exactly the released type, so the
1081    // coercion — and its coverage — lives here.
1082
1083    /// Convenience: build a deserializer over a numeric document.
1084    fn num(n: Number) -> Document {
1085        Document::Number(n)
1086    }
1087
1088    #[test]
1089    fn read_signed_integers_coerce_across_bounded_integer_sources() {
1090        let doc = num(Number::PosInt(42));
1091        assert_eq!(
1092            DocumentShapeDeserializer::new(&doc)
1093                .read_byte(&prelude::BYTE)
1094                .unwrap(),
1095            42
1096        );
1097        let doc = num(Number::NegInt(-42));
1098        assert_eq!(
1099            DocumentShapeDeserializer::new(&doc)
1100                .read_byte(&prelude::BYTE)
1101                .unwrap(),
1102            -42
1103        );
1104
1105        let doc = num(Number::PosInt(1234));
1106        assert_eq!(
1107            DocumentShapeDeserializer::new(&doc)
1108                .read_short(&prelude::SHORT)
1109                .unwrap(),
1110            1234
1111        );
1112
1113        let doc = num(Number::PosInt(i32::MAX as u64));
1114        assert_eq!(
1115            DocumentShapeDeserializer::new(&doc)
1116                .read_integer(&prelude::INTEGER)
1117                .unwrap(),
1118            i32::MAX
1119        );
1120        let doc = num(Number::NegInt(i32::MIN as i64));
1121        assert_eq!(
1122            DocumentShapeDeserializer::new(&doc)
1123                .read_integer(&prelude::INTEGER)
1124                .unwrap(),
1125            i32::MIN
1126        );
1127
1128        let doc = num(Number::PosInt(i64::MAX as u64));
1129        assert_eq!(
1130            DocumentShapeDeserializer::new(&doc)
1131                .read_long(&prelude::LONG)
1132                .unwrap(),
1133            i64::MAX
1134        );
1135    }
1136
1137    #[test]
1138    fn read_byte_overflow_populates_target_and_value() {
1139        let doc = num(Number::PosInt(200));
1140        let err = DocumentShapeDeserializer::new(&doc)
1141            .read_byte(&prelude::BYTE)
1142            .unwrap_err();
1143        match err {
1144            SerdeError::NumericCoercionOverflow { target, value } => {
1145                assert_eq!(target, "byte");
1146                assert_eq!(value, "200");
1147            }
1148            other => panic!("expected NumericCoercionOverflow, got {other:?}"),
1149        }
1150    }
1151
1152    #[test]
1153    fn read_signed_integers_overflow_at_the_edges() {
1154        // One below i32::MIN: `-(i32::MAX + 1) == i32::MIN` exactly, so
1155        // we need one further to overflow.
1156        let doc = num(Number::NegInt(i64::from(i32::MIN) - 1));
1157        assert!(matches!(
1158            DocumentShapeDeserializer::new(&doc)
1159                .read_integer(&prelude::INTEGER)
1160                .unwrap_err(),
1161            SerdeError::NumericCoercionOverflow { .. }
1162        ));
1163
1164        let doc = num(Number::PosInt(u64::MAX));
1165        assert!(matches!(
1166            DocumentShapeDeserializer::new(&doc)
1167                .read_long(&prelude::LONG)
1168                .unwrap_err(),
1169            SerdeError::NumericCoercionOverflow { .. }
1170        ));
1171    }
1172
1173    #[test]
1174    fn read_byte_on_non_numeric_is_type_mismatch() {
1175        let doc = Document::Bool(true);
1176        assert!(matches!(
1177            DocumentShapeDeserializer::new(&doc)
1178                .read_byte(&prelude::BYTE)
1179                .unwrap_err(),
1180            SerdeError::TypeMismatch { .. }
1181        ));
1182    }
1183
1184    #[test]
1185    fn integer_reads_accept_integral_floats_and_reject_fractional_ones() {
1186        let read_all = |value: f64| {
1187            let doc = num(Number::Float(value));
1188            let de = || DocumentShapeDeserializer::new(&doc);
1189            (
1190                de().read_byte(&prelude::BYTE),
1191                de().read_short(&prelude::SHORT),
1192                de().read_integer(&prelude::INTEGER),
1193                de().read_long(&prelude::LONG),
1194                de().read_big_integer(&prelude::BIG_INTEGER),
1195            )
1196        };
1197        let (b, sh, i, l, bi) = read_all(42.0);
1198        assert_eq!(
1199            (b.unwrap(), sh.unwrap(), i.unwrap(), l.unwrap()),
1200            (42, 42, 42, 42)
1201        );
1202        assert_eq!(bi.unwrap().as_ref(), "42");
1203        assert_eq!(read_all(-0.0).2.unwrap(), 0);
1204        assert_eq!(read_all(-0.0).4.unwrap().as_ref(), "0");
1205
1206        // Never truncated: a fractional or non-finite float is lossy for every integer read.
1207        for value in [42.7, -0.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1208            let (b, sh, i, l, bi) = read_all(value);
1209            for err in [b.err(), sh.err(), i.err(), l.err(), bi.err()] {
1210                assert!(
1211                    matches!(err, Some(SerdeError::NumericCoercionLossy { .. })),
1212                    "{value}: {err:?}"
1213                );
1214            }
1215        }
1216    }
1217
1218    #[test]
1219    fn integral_float_range_checks_are_exact() {
1220        let long = |v: f64| {
1221            DocumentShapeDeserializer::new(&num(Number::Float(v))).read_long(&prelude::LONG)
1222        };
1223        let int = |v: f64| {
1224            DocumentShapeDeserializer::new(&num(Number::Float(v))).read_integer(&prelude::INTEGER)
1225        };
1226        const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
1227        // -2^63 is i64::MIN exactly; 2^63 is what `i64::MAX as f64` rounds to and is out of range.
1228        assert_eq!(long(-TWO_POW_63).unwrap(), i64::MIN);
1229        assert!(matches!(
1230            long(TWO_POW_63),
1231            Err(SerdeError::NumericCoercionOverflow { .. })
1232        ));
1233        assert_eq!(int(2_147_483_647.0).unwrap(), i32::MAX);
1234        assert_eq!(int(-2_147_483_648.0).unwrap(), i32::MIN);
1235        assert!(matches!(
1236            int(2_147_483_648.0),
1237            Err(SerdeError::NumericCoercionOverflow { .. })
1238        ));
1239        // Beyond i64, an integral float is still a bigInteger, digit for digit.
1240        let big = DocumentShapeDeserializer::new(&num(Number::Float(1e20)))
1241            .read_big_integer(&prelude::BIG_INTEGER)
1242            .unwrap();
1243        assert_eq!(big.as_ref(), "100000000000000000000");
1244        let big = DocumentShapeDeserializer::new(&num(Number::Float(TWO_POW_63)))
1245            .read_big_integer(&prelude::BIG_INTEGER)
1246            .unwrap();
1247        assert_eq!(big.as_ref(), "9223372036854775808");
1248    }
1249
1250    #[test]
1251    fn read_double_and_float_widen_from_integer_sources() {
1252        // Integer → float IS allowed: the integer value is exactly
1253        // representable. Only the reverse crossover is forbidden.
1254        let doc = num(Number::PosInt(42));
1255        assert_eq!(
1256            DocumentShapeDeserializer::new(&doc)
1257                .read_double(&prelude::DOUBLE)
1258                .unwrap(),
1259            42.0
1260        );
1261        assert_eq!(
1262            DocumentShapeDeserializer::new(&doc)
1263                .read_float(&prelude::FLOAT)
1264                .unwrap(),
1265            42.0_f32
1266        );
1267
1268        let doc = num(Number::NegInt(-42));
1269        assert_eq!(
1270            DocumentShapeDeserializer::new(&doc)
1271                .read_double(&prelude::DOUBLE)
1272                .unwrap(),
1273            -42.0
1274        );
1275
1276        let doc = num(Number::Float(1.5));
1277        assert_eq!(
1278            DocumentShapeDeserializer::new(&doc)
1279                .read_float(&prelude::FLOAT)
1280                .unwrap(),
1281            1.5_f32
1282        );
1283    }
1284
1285    #[test]
1286    fn read_double_carries_non_finite_floats_intact() {
1287        // The codec layer owns wire-format rendering of NaN / Infinity;
1288        // the Document path must simply not mangle them.
1289        let doc = num(Number::Float(f64::NAN));
1290        assert!(DocumentShapeDeserializer::new(&doc)
1291            .read_double(&prelude::DOUBLE)
1292            .unwrap()
1293            .is_nan());
1294
1295        let doc = num(Number::Float(f64::INFINITY));
1296        assert_eq!(
1297            DocumentShapeDeserializer::new(&doc)
1298                .read_double(&prelude::DOUBLE)
1299                .unwrap(),
1300            f64::INFINITY
1301        );
1302
1303        let doc = num(Number::Float(f64::NEG_INFINITY));
1304        assert_eq!(
1305            DocumentShapeDeserializer::new(&doc)
1306                .read_double(&prelude::DOUBLE)
1307                .unwrap(),
1308            f64::NEG_INFINITY
1309        );
1310    }
1311
1312    #[test]
1313    fn read_big_integer_truncates_a_decimal_string_as_text() {
1314        // Truncation happens *as a string*, NOT via f64 (which would
1315        // lose precision past 2^53).
1316        let doc = Document::String("12345678901234567890.123".to_owned());
1317        assert_eq!(
1318            DocumentShapeDeserializer::new(&doc)
1319                .read_big_integer(&prelude::BIG_INTEGER)
1320                .unwrap()
1321                .as_ref(),
1322            "12345678901234567890"
1323        );
1324    }
1325
1326    #[test]
1327    fn read_big_integer_expands_scientific_notation() {
1328        // Splitting at the first 'e' would return "1" for "1.23e10" —
1329        // off by ten orders of magnitude.
1330        let doc = Document::String("1.23e10".to_owned());
1331        assert_eq!(
1332            DocumentShapeDeserializer::new(&doc)
1333                .read_big_integer(&prelude::BIG_INTEGER)
1334                .unwrap()
1335                .as_ref(),
1336            "12300000000"
1337        );
1338
1339        let doc = Document::String("1e30".to_owned());
1340        assert_eq!(
1341            DocumentShapeDeserializer::new(&doc)
1342                .read_big_integer(&prelude::BIG_INTEGER)
1343                .unwrap()
1344                .as_ref(),
1345            "1000000000000000000000000000000"
1346        );
1347    }
1348
1349    #[test]
1350    fn read_big_integer_errors_on_unmaterializable_exponent() {
1351        // A pathological exponent must error, not allocate gigabytes or
1352        // return a wrong value.
1353        let doc = Document::String("1e1000000000".to_owned());
1354        let err = DocumentShapeDeserializer::new(&doc)
1355            .read_big_integer(&prelude::BIG_INTEGER)
1356            .unwrap_err();
1357        assert!(
1358            err.to_string().contains("too large"),
1359            "expected a 'too large' error, got: {err}"
1360        );
1361    }
1362
1363    #[test]
1364    fn read_big_integer_rejects_non_numeric_text() {
1365        let doc = Document::String("not a number".to_owned());
1366        assert!(matches!(
1367            DocumentShapeDeserializer::new(&doc)
1368                .read_big_integer(&prelude::BIG_INTEGER)
1369                .unwrap_err(),
1370            SerdeError::InvalidInput { .. }
1371        ));
1372    }
1373
1374    #[test]
1375    fn read_big_numbers_coerce_from_integer_sources() {
1376        let doc = num(Number::PosInt(7));
1377        assert_eq!(
1378            DocumentShapeDeserializer::new(&doc)
1379                .read_big_integer(&prelude::BIG_INTEGER)
1380                .unwrap()
1381                .as_ref(),
1382            "7"
1383        );
1384        assert_eq!(
1385            DocumentShapeDeserializer::new(&doc)
1386                .read_big_decimal(&prelude::BIG_DECIMAL)
1387                .unwrap()
1388                .as_ref(),
1389            "7"
1390        );
1391
1392        let doc = num(Number::NegInt(-7));
1393        assert_eq!(
1394            DocumentShapeDeserializer::new(&doc)
1395                .read_big_decimal(&prelude::BIG_DECIMAL)
1396                .unwrap()
1397                .as_ref(),
1398            "-7"
1399        );
1400    }
1401
1402    #[test]
1403    fn read_big_decimal_rejects_non_finite_floats() {
1404        // Distinct from `read_big_integer`: Float→BigDecimal is accepted
1405        // for finite values (a string-format conversion), but non-finite
1406        // values have no decimal form.
1407        let doc = num(Number::Float(f64::INFINITY));
1408        let err = DocumentShapeDeserializer::new(&doc)
1409            .read_big_decimal(&prelude::BIG_DECIMAL)
1410            .unwrap_err();
1411        match err {
1412            SerdeError::Custom { message } => assert!(message.contains("non-finite")),
1413            other => panic!("expected Custom non-finite error, got {other:?}"),
1414        }
1415    }
1416
1417    #[test]
1418    fn big_number_reads_reject_structural_variants() {
1419        let doc = Document::Array(vec![]);
1420        assert!(matches!(
1421            DocumentShapeDeserializer::new(&doc)
1422                .read_big_integer(&prelude::BIG_INTEGER)
1423                .unwrap_err(),
1424            SerdeError::TypeMismatch { .. }
1425        ));
1426        assert!(matches!(
1427            DocumentShapeDeserializer::new(&doc)
1428                .read_big_decimal(&prelude::BIG_DECIMAL)
1429                .unwrap_err(),
1430            SerdeError::TypeMismatch { .. }
1431        ));
1432        assert!(matches!(
1433            DocumentShapeDeserializer::new(&doc)
1434                .read_double(&prelude::DOUBLE)
1435                .unwrap_err(),
1436            SerdeError::TypeMismatch { .. }
1437        ));
1438    }
1439
1440    #[test]
1441    fn big_integer_legacy_string_round_trips() {
1442        // Full circle: BigInteger -> legacy String representation ->
1443        // BigInteger.
1444        let bi = BigInteger::from_str("98765432109876543210").unwrap();
1445        let doc = Document::String(bi.as_ref().to_owned());
1446        assert_eq!(
1447            DocumentShapeDeserializer::new(&doc)
1448                .read_big_integer(&prelude::BIG_INTEGER)
1449                .unwrap(),
1450            bi
1451        );
1452    }
1453
1454    // -- big_decimal_to_integer_string (moved off `BigDecimal`) ----------
1455
1456    #[test]
1457    fn big_decimal_to_integer_string_truncates_and_expands() {
1458        // (input, expected truncated-toward-zero integer string)
1459        let cases = [
1460            ("0", "0"),
1461            ("123", "123"),
1462            ("123.99", "123"), // fractional digits dropped
1463            ("-123.99", "-123"),
1464            ("0.5", "0"),
1465            ("-0.5", "0"),              // negative zero normalizes to "0"
1466            ("1.23e10", "12300000000"), // exponent expanded, magnitude kept
1467            ("1.23e1", "12"),           // 12.3 -> 12
1468            ("1.5e1", "15"),
1469            ("1e3", "1000"),
1470            ("5e-3", "0"), // 0.005 -> 0
1471            ("-1.23e10", "-12300000000"),
1472            ("10.0", "10"),
1473            ("+123", "123"),            // explicit positive sign removed
1474            (".5", "0"),                // omitted integer digit run
1475            ("-.5", "0"),               // omitted integer digits, negative zero normalized
1476            ("1.", "1"),                // omitted fractional digit run
1477            ("+1.23e1", "12"),          // sign and exponent expansion
1478            ("00123", "123"),           // leading zeros normalized
1479            ("1.23E10", "12300000000"), // uppercase E
1480        ];
1481        for (input, expected) in cases {
1482            let bd = BigDecimal::from_str(input).unwrap();
1483            assert_eq!(
1484                big_decimal_to_integer_string(&bd).as_deref(),
1485                Some(expected),
1486                "big_decimal_to_integer_string({input:?})"
1487            );
1488        }
1489    }
1490
1491    #[test]
1492    fn big_decimal_to_integer_string_guards_pathological_exponent() {
1493        // Exponent fits in i64 but is absurdly large: refuse to
1494        // materialize rather than allocating ~10^9 bytes.
1495        assert_eq!(
1496            big_decimal_to_integer_string(&BigDecimal::from_str("1e1000000000").unwrap()),
1497            None
1498        );
1499        // The exponent overflows i64, while the fraction-adjusted scale is
1500        // still within the range accepted by BigDecimal.
1501        assert_eq!(
1502            big_decimal_to_integer_string(
1503                &BigDecimal::from_str("1.0e9223372036854775808").unwrap()
1504            ),
1505            None
1506        );
1507        // An i64-underflowing negative exponent cannot reach this helper:
1508        // BigDecimal rejects it because its resulting scale is out of range.
1509    }
1510}
1511
1512/// Tests for the [`ShapeDeserializer::skip_value`] contract.
1513///
1514/// This deserializer walks an already-parsed document tree and hands each member its own
1515/// sub-deserializer, so skipping must be a no-op: there is no shared cursor to advance.
1516#[cfg(test)]
1517mod skip_value_contract {
1518    use super::*;
1519    use crate::serde::ShapeDeserializer;
1520    use crate::{shape_id, Schema, ShapeType};
1521    use std::collections::HashMap;
1522
1523    static NAME: Schema<'static> = Schema::new_member(
1524        shape_id!("smithy.example", "Person", "name"),
1525        ShapeType::String,
1526        "name",
1527        0,
1528    );
1529    static AGE: Schema<'static> = Schema::new_member(
1530        shape_id!("smithy.example", "Person", "age"),
1531        ShapeType::Integer,
1532        "age",
1533        1,
1534    );
1535    static PERSON: Schema<'static> = Schema::new_struct(
1536        shape_id!("smithy.example", "Person"),
1537        ShapeType::Structure,
1538        &[&NAME, &AGE],
1539    );
1540
1541    #[test]
1542    fn skipping_one_member_leaves_the_others_readable() {
1543        let doc = Document::Object(HashMap::from([
1544            ("name".to_string(), Document::String("Alex".into())),
1545            (
1546                "age".to_string(),
1547                Document::Number(aws_smithy_types::Number::PosInt(30)),
1548            ),
1549        ]));
1550        let mut deser = DocumentShapeDeserializer::new(&doc);
1551        let mut age = None;
1552        deser
1553            .read_struct(&PERSON, &mut |member, d| {
1554                match member.member_index() {
1555                    Some(0) => d.skip_value()?,
1556                    Some(1) => age = Some(d.read_integer(member)?),
1557                    _ => {}
1558                }
1559                Ok(())
1560            })
1561            .unwrap();
1562        assert_eq!(age, Some(30));
1563    }
1564
1565    #[test]
1566    fn skipping_an_aggregate_member_is_a_no_op() {
1567        // Nested content must not disturb anything, since the sub-deserializer is a
1568        // separate view over its own node.
1569        let doc = Document::Object(HashMap::from([
1570            (
1571                "name".to_string(),
1572                Document::Array(vec![
1573                    Document::String("nested".into()),
1574                    Document::Object(HashMap::from([(
1575                        "age".to_string(),
1576                        Document::Number(aws_smithy_types::Number::PosInt(999)),
1577                    )])),
1578                ]),
1579            ),
1580            (
1581                "age".to_string(),
1582                Document::Number(aws_smithy_types::Number::PosInt(30)),
1583            ),
1584        ]));
1585        let mut deser = DocumentShapeDeserializer::new(&doc);
1586        let mut age = None;
1587        deser
1588            .read_struct(&PERSON, &mut |member, d| {
1589                match member.member_index() {
1590                    Some(0) => d.skip_value()?,
1591                    Some(1) => age = Some(d.read_integer(member)?),
1592                    _ => {}
1593                }
1594                Ok(())
1595            })
1596            .unwrap();
1597        assert_eq!(age, Some(30), "the decoy nested `age` must not be read");
1598    }
1599
1600    #[test]
1601    fn skip_value_does_not_consume_the_cursor() {
1602        // Skipping then reading the same value must still work: the override must not
1603        // have side effects.
1604        let doc = Document::String("still here".into());
1605        let mut deser = DocumentShapeDeserializer::new(&doc);
1606        let dynamic: &mut dyn ShapeDeserializer = &mut deser;
1607        dynamic.skip_value().unwrap();
1608        assert_eq!(deser.read_string(&NAME).unwrap(), "still here");
1609    }
1610}