Skip to main content

decimal_scaled/support/
serde_helpers.rs

1// SPDX-FileCopyrightText: 2026 John Moxley
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `serde` integration for every decimal width.
5//!
6//! D38 has a dedicated [`Serialize`] / [`Deserialize`] pair plus the
7//! richer [`decimal_serde::DecimalVisitor`] used for `#[serde(with =
8//! "...")]` field annotations. Every wide tier (D57 through D1232)
9//! uses a slimmer implementation emitted by `decl_wide_serde!`: a
10//! decimal-string wire format for human-readable serializers and a
11//! little-endian limb-bytes wire format for binary serializers.
12//! Cross-tier wire-format parity is intentional — a D38 produced
13//! at SCALE = 12 serialises to the same string as a D76 at SCALE =
14//! 12 carrying the same logical value.
15//!
16//!
17//! # Wire format
18//!
19//! `D38<SCALE>` chooses its wire encoding based on the serializer's
20//! [`serde::Serializer::is_human_readable`] flag:
21//!
22//! - **Human-readable formats** (JSON, TOML, YAML): a base-10 integer
23//! string of the underlying `i128` storage value. For example,
24//! `D38s12::ONE` (storage `1_000_000_000_000`) serialises as the
25//! JSON string `"1000000000000"`. This is not a decimal string like
26//! `"1.0"` — that is the job of `Display`, not the wire format.
27//!
28//! A string rather than a JSON number is used because JSON numbers
29//! are effectively `f64` in most runtimes (max safe integer =
30//! `2^53 - 1`), while `i128` storage requires up to 127 bits. A
31//! BigInt-compatible integer string is the only lossless option for
32//! interoperability with JavaScript, where `BigInt(s).toString()`
33//! round-trips the same digits.
34//!
35//! - **Binary formats** (postcard, bincode, etc.): 16 little-endian
36//! bytes from `i128::to_le_bytes`. Compact and endian-canonical.
37//!
38//! On deserialise, the internal `DecimalVisitor` handles both wire forms plus
39//! `visit_i64` / `visit_u64` / `visit_i128` / `visit_u128` callbacks,
40//! which are used when the underlying format yields a native integer.
41//! The integer is interpreted directly as the scaled `i128` storage.
42
43use core::marker::PhantomData;
44
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
46
47#[cfg(feature = "alloc")]
48use alloc::string::ToString;
49
50// ── Serialize ─────────────────────────────────────────────────────────
51
52impl<const SCALE: u32> Serialize for crate::D<crate::int::types::Int<2>, SCALE> {
53    /// Serialise `self` as a base-10 integer string for human-readable
54    /// formats, or as 16 little-endian bytes for binary formats.
55    ///
56    /// # Precision
57    ///
58    /// Strict: all arithmetic is integer-only; result is bit-exact.
59    #[inline]
60    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
61        if serializer.is_human_readable() {
62            // Formatting an i128 as a decimal string requires heap
63            // allocation. Every real human-readable format already
64            // depends on alloc, so this is not a practical constraint.
65            #[cfg(feature = "alloc")]
66            {
67                serializer.serialize_str(&self.0.to_string())
68            }
69            // Human-readable serialisation without alloc is not
70            // supported. A 40-byte stack buffer would technically
71            // suffice for an i128 decimal, but no real target combines
72            // `no_std + !alloc + serde + human-readable format`.
73            #[cfg(not(feature = "alloc"))]
74            {
75                let _ = serializer;
76                Err(serde::ser::Error::custom(
77                    "decimal-scaled: human-readable serialisation requires the `alloc` feature",
78                ))
79            }
80        } else {
81            // Binary path: emit the raw i128 as 16 little-endian bytes.
82            serializer.serialize_bytes(&self.0.as_i128().to_le_bytes())
83        }
84    }
85}
86
87// ── Deserialize ───────────────────────────────────────────────────────
88
89impl<'de, const SCALE: u32> Deserialize<'de> for crate::D<crate::int::types::Int<2>, SCALE> {
90    /// Deserialise from a base-10 integer string (human-readable
91    /// formats), 16 little-endian bytes (binary formats), or a native
92    /// integer (self-describing binary formats such as CBOR).
93    ///
94    /// Human-readable formats route via `deserialize_any` so a JSON
95    /// string, JSON number, or TOML integer all reach the correct
96    /// visitor branch. Binary formats that are not self-describing
97    /// (postcard, bincode) route via `deserialize_bytes` directly.
98    ///
99    /// # Precision
100    ///
101    /// Strict: all arithmetic is integer-only; result is bit-exact.
102    #[inline]
103    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
104        let visitor = decimal_serde::DecimalVisitor::<SCALE>(PhantomData);
105        if deserializer.is_human_readable() {
106            deserializer.deserialize_any(visitor)
107        } else {
108            deserializer.deserialize_bytes(visitor)
109        }
110    }
111}
112
113// ── Free-function helpers and visitor ─────────────────────────────────
114
115/// Serde helper module for `#[serde(with = "...")]` field annotations.
116///
117/// Use this module when you want to control serialisation of a `D38`
118/// field on a struct that derives `Serialize` / `Deserialize`:
119///
120/// ```ignore
121/// use decimal_scaled::D38;
122///
123/// #[derive(serde::Serialize, serde::Deserialize)]
124/// struct MyStruct {
125/// #[serde(with = "decimal_scaled::serde_helpers::decimal_serde")]
126/// length: D38<12>,
127/// }
128/// ```
129///
130/// The free functions delegate to the inherent `Serialize` /
131/// `Deserialize` impls; they exist so users can annotate
132/// `#[serde(with = ...)]` on fields in generic containing types or in
133/// newtype wrappers where the trait impl may be shadowed.
134pub mod decimal_serde {
135    use super::{Deserialize, Deserializer, PhantomData, Serialize, Serializer, Visitor};
136
137    /// Serialise `value` using the `D38` wire format.
138    ///
139    /// Intended for use under `#[serde(serialize_with = "...")]` or
140    /// `#[serde(with = "...")]`.
141    ///
142    /// # Precision
143    ///
144    /// Strict: all arithmetic is integer-only; result is bit-exact.
145    #[inline]
146    pub fn serialize<const SCALE: u32, S: Serializer>(
147        value: &crate::D<crate::int::types::Int<2>, SCALE>,
148        serializer: S,
149    ) -> Result<S::Ok, S::Error> {
150        value.serialize(serializer)
151    }
152
153    /// Deserialise a `D38` using the wire format.
154    ///
155    /// Intended for use under `#[serde(deserialize_with = "...")]` or
156    /// `#[serde(with = "...")]`.
157    ///
158    /// # Precision
159    ///
160    /// Strict: all arithmetic is integer-only; result is bit-exact.
161    #[inline]
162    pub fn deserialize<'de, const SCALE: u32, D: Deserializer<'de>>(
163        deserializer: D,
164    ) -> Result<crate::D<crate::int::types::Int<2>, SCALE>, D::Error> {
165        crate::D::<crate::int::types::Int<2>, SCALE>::deserialize(deserializer)
166    }
167
168    /// Visitor that backs [`deserialize`]. Public so external helper
169    /// modules can reuse it under custom `#[serde(deserialize_with)]`
170    /// shapes.
171    ///
172    /// Accepted inputs:
173    ///
174    /// - `&str` / borrowed string: parsed as a strict base-10 `i128`
175    /// integer (no decimal point, no whitespace, no leading `+`).
176    /// - `&[u8]` / byte buf: interpreted as exactly 16 little-endian
177    /// `i128` bytes.
178    /// - Native integer (`i8` through `i128`, `u8` through `u128`):
179    /// widened into `i128` storage directly. The integer is treated as
180    /// the scaled storage value, not as a logical decimal value.
181    pub struct DecimalVisitor<const SCALE: u32>(pub PhantomData<()>);
182
183    impl<'de, const SCALE: u32> Visitor<'de> for DecimalVisitor<SCALE> {
184        type Value = crate::D<crate::int::types::Int<2>, SCALE>;
185
186        fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
187            formatter.write_str(
188                "a base-10 i128 integer string, 16 little-endian bytes, \
189                 or a native integer",
190            )
191        }
192
193        // ── String wire form (human-readable) ─────────────────────────
194
195        fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Self::Value, E> {
196            // The wire format is a strict base-10 i128 integer literal
197            // matching `-?[0-9]+`. No whitespace, no leading `+`, no
198            // decimal point, no scientific notation, no underscores.
199            // Display's canonical decimal form (e.g. "1.500") is NOT
200            // accepted here — that belongs to the FromStr parse path.
201            //
202            // A leading `+` is rejected explicitly to keep one canonical
203            // wire form per value, matching JavaScript BigInt.toString()
204            // output which is never `+`-prefixed.
205            let bytes = text.as_bytes();
206            if bytes.is_empty() {
207                return Err(serde::de::Error::custom(
208                    "decimal-scaled: empty string is not a valid i128 wire",
209                ));
210            }
211            if bytes[0] == b'+' {
212                return Err(serde::de::Error::custom(
213                    "decimal-scaled: leading `+` is not part of the canonical wire format",
214                ));
215            }
216            text.parse::<i128>().map(|parsed| crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(parsed))).map_err(|_| {
217                serde::de::Error::custom("decimal-scaled: expected a base-10 i128 integer string")
218            })
219        }
220
221        fn visit_borrowed_str<E: serde::de::Error>(self, text: &'de str) -> Result<Self::Value, E> {
222            self.visit_str(text)
223        }
224
225        #[cfg(feature = "alloc")]
226        fn visit_string<E: serde::de::Error>(
227            self,
228            text: alloc::string::String,
229        ) -> Result<Self::Value, E> {
230            self.visit_str(&text)
231        }
232
233        // ── Bytes wire form (binary) ───────────────────────────────────
234
235        fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
236            // Require exactly 16 bytes: the little-endian i128 layout.
237            let le_bytes: [u8; 16] = bytes.try_into().map_err(|_| {
238                serde::de::Error::invalid_length(
239                    bytes.len(),
240                    &"exactly 16 little-endian bytes for an i128",
241                )
242            })?;
243            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from_le_bytes(le_bytes))))
244        }
245
246        fn visit_borrowed_bytes<E: serde::de::Error>(self, bytes: &'de [u8]) -> Result<Self::Value, E> {
247            self.visit_bytes(bytes)
248        }
249
250        #[cfg(feature = "alloc")]
251        fn visit_byte_buf<E: serde::de::Error>(
252            self,
253            bytes: alloc::vec::Vec<u8>,
254        ) -> Result<Self::Value, E> {
255            self.visit_bytes(&bytes)
256        }
257
258        // ── Native-integer wire forms ──────────────────────────────────
259        //
260        // These branches are entered when the underlying format yields a
261        // typed integer rather than a string or byte slice (e.g. CBOR
262        // major types 0/1, MessagePack integer family). The value is
263        // interpreted as the scaled i128 storage, matching the binary
264        // serialise path.
265
266        fn visit_i8<E: serde::de::Error>(self, value: i8) -> Result<Self::Value, E> {
267            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
268        }
269
270        fn visit_i16<E: serde::de::Error>(self, value: i16) -> Result<Self::Value, E> {
271            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
272        }
273
274        fn visit_i32<E: serde::de::Error>(self, value: i32) -> Result<Self::Value, E> {
275            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
276        }
277
278        fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
279            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
280        }
281
282        fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
283            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(value)))
284        }
285
286        fn visit_u8<E: serde::de::Error>(self, value: u8) -> Result<Self::Value, E> {
287            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
288        }
289
290        fn visit_u16<E: serde::de::Error>(self, value: u16) -> Result<Self::Value, E> {
291            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
292        }
293
294        fn visit_u32<E: serde::de::Error>(self, value: u32) -> Result<Self::Value, E> {
295            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
296        }
297
298        fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
299            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(i128::from(value))))
300        }
301
302        fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
303            // u128 values above i128::MAX cannot be represented; reject
304            // explicitly rather than wrapping silently.
305            i128::try_from(value).map(|narrowed| crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(narrowed))).map_err(|_| {
306                serde::de::Error::custom("decimal-scaled: u128 value exceeds i128 storage range")
307            })
308        }
309
310        // ── Float inputs are not a supported wire format ───────────────
311        //
312        // The wire format is integer-string or little-endian bytes.
313        // Floats are not accepted. If a human-edited TOML file contains
314        // a bare integer that fits in i64, the format's deserializer
315        // routes via visit_i64 / visit_u64 / visit_i128 above, which is
316        // correct. A genuine f64 value (e.g. 1.5) is rejected as
317        // "expected i128 integer".
318    }
319}
320
321// ── Tests ──────────────────────────────────────────────────────────────
322
323#[cfg(all(test, feature = "alloc", feature = "serde"))]
324mod tests {
325    use super::*;
326    use crate::types::widths::D38s12;
327    use alloc::format;
328    use serde::de::IntoDeserializer;
329    use serde::de::value::{Error as DeError, StrDeserializer};
330
331    // ── String wire form round-trips ──────────────────────────────────
332
333    /// `"0"` deserialises to `ZERO` via the canonical-string path.
334    #[test]
335    fn deserialize_canonical_zero_string() {
336        let deserializer: StrDeserializer<DeError> = "0".into_deserializer();
337        let value: D38s12 = D38s12::deserialize(deserializer).unwrap();
338        assert_eq!(value, D38s12::ZERO);
339    }
340
341    /// The visitor accepts the scaled integer representation of `ONE`
342    /// (`10^12` for `D38s12`) when fed via `visit_str`.
343    #[test]
344    fn visitor_accepts_scaled_one_str() {
345        let visitor = decimal_serde::DecimalVisitor::<12>(PhantomData);
346        let value: D38s12 = <_ as Visitor>::visit_str::<DeError>(visitor, "1000000000000").unwrap();
347        assert_eq!(value, D38s12::ONE);
348    }
349
350    /// The visitor rejects a decimal-point string. `"1.5"` is the
351    /// Display format, not the wire format.
352    #[test]
353    fn visitor_rejects_decimal_point_str() {
354        let visitor = decimal_serde::DecimalVisitor::<12>(PhantomData);
355        let parsed: Result<D38s12, _> = <_ as Visitor>::visit_str::<DeError>(visitor, "1.5");
356        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
357    }
358
359    // ── Native-integer wire form round-trips ──────────────────────────
360
361    /// `visit_i64` interprets the input as scaled storage; `-5` stored
362    /// directly produces a `D38` whose raw bits are `-5`.
363    #[test]
364    fn visitor_accepts_i64_as_storage() {
365        let visitor = decimal_serde::DecimalVisitor::<12>(PhantomData);
366        let value: D38s12 = <_ as Visitor>::visit_i64::<DeError>(visitor, -5).unwrap();
367        assert_eq!(value.to_bits(), -5);
368    }
369
370    /// `visit_u64` with `u64::MAX` widens cleanly into `i128` storage.
371    #[test]
372    fn visitor_accepts_u64_max() {
373        let visitor = decimal_serde::DecimalVisitor::<12>(PhantomData);
374        let value: D38s12 = <_ as Visitor>::visit_u64::<DeError>(visitor, u64::MAX).unwrap();
375        assert_eq!(value.to_bits(), u64::MAX as i128);
376    }
377
378    /// `visit_u128` past `i128::MAX` yields an explicit out-of-range
379    /// error rather than wrapping silently.
380    #[test]
381    fn visitor_rejects_u128_above_i128_max() {
382        let visitor = decimal_serde::DecimalVisitor::<12>(PhantomData);
383        let parsed: Result<D38s12, _> =
384            <_ as Visitor>::visit_u128::<DeError>(visitor, (i128::MAX as u128) + 1);
385        assert!(
386            parsed.is_err(),
387            "expected overflow reject; got Ok({:?})",
388            parsed
389        );
390    }
391
392    // ── JSON round-trips ──────────────────────────────────────────────
393
394    /// `D38s12::ONE` serialises as the JSON string `"1000000000000"`.
395    /// This is the BigInt-compatible wire form, not the Display form
396    /// `"1.000000000000"`.
397    #[test]
398    fn json_one_serialises_as_scaled_integer_string() {
399        let json = serde_json::to_string(&D38s12::ONE).unwrap();
400        assert_eq!(json, "\"1000000000000\"");
401    }
402
403    #[test]
404    fn json_zero_serialises_as_zero_string() {
405        let json = serde_json::to_string(&D38s12::ZERO).unwrap();
406        assert_eq!(json, "\"0\"");
407    }
408
409    #[test]
410    fn json_one_round_trips() {
411        let json = serde_json::to_string(&D38s12::ONE).unwrap();
412        let back: D38s12 = serde_json::from_str(&json).unwrap();
413        assert_eq!(back, D38s12::ONE);
414    }
415
416    #[test]
417    fn json_zero_round_trips() {
418        let json = serde_json::to_string(&D38s12::ZERO).unwrap();
419        let back: D38s12 = serde_json::from_str(&json).unwrap();
420        assert_eq!(back, D38s12::ZERO);
421    }
422
423    /// Negative values round-trip through JSON. `try_from(-5_i32)` stores
424    /// `-5 * 10^12 = -5_000_000_000_000`.
425    #[test]
426    fn json_negative_round_trips() {
427        let value = D38s12::try_from(-5_i32).unwrap();
428        let json = serde_json::to_string(&value).unwrap();
429        assert_eq!(json, "\"-5000000000000\"");
430        let back: D38s12 = serde_json::from_str(&json).unwrap();
431        assert_eq!(back, value);
432        assert_eq!(back.to_bits(), -5_000_000_000_000_i128);
433    }
434
435    /// `D38::MAX` and `D38::MIN` round-trip exactly through the
436    /// JSON-string wire format.
437    #[test]
438    fn json_max_round_trips() {
439        let json = serde_json::to_string(&D38s12::MAX).unwrap();
440        let back: D38s12 = serde_json::from_str(&json).unwrap();
441        assert_eq!(back, D38s12::MAX);
442    }
443
444    #[test]
445    fn json_min_round_trips() {
446        let json = serde_json::to_string(&D38s12::MIN).unwrap();
447        let back: D38s12 = serde_json::from_str(&json).unwrap();
448        assert_eq!(back, D38s12::MIN);
449    }
450
451    /// The JSON string representation matches `i128::to_string` exactly.
452    /// On the JavaScript side, `BigInt(json).toString()` reproduces the
453    /// same digits.
454    #[test]
455    fn json_string_matches_i128_to_string() {
456        let raw: i128 = -123_456_789_012_345_678_901_234_567_890_i128;
457        let value = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(raw));
458        let json = serde_json::to_string(&value).unwrap();
459        assert_eq!(json, format!("\"{}\"", raw));
460    }
461
462    // ── JSON: malformed input rejection ───────────────────────────────
463
464    #[test]
465    fn json_rejects_decimal_point_string() {
466        let parsed: Result<D38s12, _> = serde_json::from_str("\"1.5\"");
467        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
468    }
469
470    #[test]
471    fn json_rejects_scientific_notation_string() {
472        let parsed: Result<D38s12, _> = serde_json::from_str("\"1e6\"");
473        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
474    }
475
476    #[test]
477    fn json_rejects_not_a_number_string() {
478        let parsed: Result<D38s12, _> = serde_json::from_str("\"not-a-number\"");
479        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
480    }
481
482    #[test]
483    fn json_rejects_empty_string() {
484        let parsed: Result<D38s12, _> = serde_json::from_str("\"\"");
485        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
486    }
487
488    #[test]
489    fn json_rejects_leading_whitespace_string() {
490        // `i128::from_str` does not trim whitespace; the wire format
491        // requires a strict integer literal.
492        let parsed: Result<D38s12, _> = serde_json::from_str("\"  42\"");
493        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
494    }
495
496    #[test]
497    fn json_rejects_plus_prefix() {
498        let parsed: Result<D38s12, _> = serde_json::from_str("\"+42\"");
499        assert!(parsed.is_err(), "expected reject; got Ok({:?})", parsed);
500    }
501
502    /// A bare JSON integer (not a string) is accepted via `visit_i64`.
503    /// The number is interpreted as the scaled storage value.
504    #[test]
505    fn json_accepts_bare_integer_number_as_storage() {
506        let back: D38s12 = serde_json::from_str("42").unwrap();
507        assert_eq!(back.to_bits(), 42_i128);
508    }
509
510    // ── Postcard binary 16-byte LE round-trips ────────────────────────
511
512    #[test]
513    fn postcard_one_round_trips() {
514        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::ONE).unwrap();
515        // Verify the raw 16 LE bytes appear somewhere in the postcard
516        // output (postcard may prepend a varint length prefix).
517        let raw = D38s12::ONE.to_bits().as_i128().to_le_bytes();
518        assert!(bytes.windows(16).any(|window| window == raw));
519        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
520        assert_eq!(back, D38s12::ONE);
521    }
522
523    #[test]
524    fn postcard_zero_round_trips() {
525        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::ZERO).unwrap();
526        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
527        assert_eq!(back, D38s12::ZERO);
528    }
529
530    #[test]
531    fn postcard_negative_round_trips() {
532        let value = D38s12::try_from(-5_i32).unwrap();
533        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&value).unwrap();
534        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
535        assert_eq!(back, value);
536    }
537
538    #[test]
539    fn postcard_max_round_trips() {
540        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::MAX).unwrap();
541        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
542        assert_eq!(back, D38s12::MAX);
543    }
544
545    #[test]
546    fn postcard_min_round_trips() {
547        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::MIN).unwrap();
548        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
549        assert_eq!(back, D38s12::MIN);
550    }
551
552    /// The postcard payload contains the raw `i128::to_le_bytes`
553    /// representation. The first LE byte is the LSB and the last is
554    /// the MSB.
555    #[test]
556    fn postcard_byte_order_matches_le() {
557        let value = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(0x0123_4567_89AB_CDEF_FEDC_BA98_7654_3210_i128));
558        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&value).unwrap();
559        let raw = value.to_bits().as_i128().to_le_bytes();
560        let found = bytes.windows(16).position(|window| window == raw);
561        assert!(
562            found.is_some(),
563            "expected raw LE bytes embedded; got {:?}",
564            bytes
565        );
566        assert_eq!(raw[0], 0x10); // LSB of the i128
567        assert_eq!(raw[15], 0x01); // MSB of the i128
568    }
569
570    // ── Cross-format compatibility ─────────────────────────────────────
571
572    /// The JSON integer string, when parsed back to `i128` and converted
573    /// to `to_le_bytes`, matches the binary wire representation directly.
574    #[test]
575    fn cross_format_json_string_matches_le_bytes() {
576        let value = D38s12::try_from(42_i32).unwrap();
577        let json = serde_json::to_string(&value).unwrap();
578        let inner = json.trim_matches('"');
579        let parsed: i128 = inner.parse().unwrap();
580        let json_bytes = parsed.to_le_bytes();
581        let direct_bytes = value.to_bits().as_i128().to_le_bytes();
582        assert_eq!(json_bytes, direct_bytes);
583    }
584
585    /// Different SCALE values serialise identically when they share the
586    /// same raw storage. The SCALE is a compile-time type parameter and
587    /// is not encoded in the wire.
588    #[test]
589    fn cross_scale_wire_is_storage_only() {
590        let raw: i128 = 1_500_000_000_000;
591        let at_scale_12 = crate::D::<crate::int::types::Int<2>, 12>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
592        let at_scale_6 = crate::D::<crate::int::types::Int<2>, 6>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
593        assert_eq!(serde_json::to_string(&at_scale_12).unwrap(), "\"1500000000000\"");
594        assert_eq!(serde_json::to_string(&at_scale_6).unwrap(), "\"1500000000000\"");
595    }
596
597    // ── decimal_serde free-function helpers ───────────────────────────
598
599    /// The `#[serde(with = "...")]` helpers delegate to the inherent
600    /// impls and produce the correct JSON output.
601    #[test]
602    fn decimal_serde_helper_round_trips() {
603        #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
604        struct Holder {
605            #[serde(with = "crate::serde_helpers::decimal_serde")]
606            length: crate::D<crate::int::types::Int<2>, 12>,
607        }
608
609        let holder = Holder {
610            length: D38s12::try_from(7_i32).unwrap(),
611        };
612        let json = serde_json::to_string(&holder).unwrap();
613        assert_eq!(json, r#"{"length":"7000000000000"}"#);
614        let back: Holder = serde_json::from_str(&json).unwrap();
615        assert_eq!(back, holder);
616    }
617}
618
619// ─── Wide-tier serde (D57 through D1232) ────────────────────────────
620//
621// The wide-tier wire format mirrors D38's: a base-10 integer string
622// of the raw storage value for human-readable serializers, and the
623// raw little-endian limb bytes for binary serializers. The
624// implementation is intentionally slimmer than D38's — no
625// native-integer visit methods, since no native int can losslessly
626// carry the >128-bit storage anyway.
627
628/// Emits `Serialize` / `Deserialize` for a wide-tier decimal type
629/// (D57 through D1232). `$bytes_len` is `mem::size_of::<$Storage>()`
630/// (e.g. 32 for `Int<4>`).
631#[cfg(feature = "_wide-support")]
632macro_rules! decl_wide_serde {
633    ($Type:ident, $Storage:ty, $bytes_len:literal) => {
634        impl<const SCALE: u32> Serialize for $crate::types::widths::$Type<SCALE> {
635            /// Serialise as a base-10 integer string for human-
636            /// readable formats, or as `$bytes_len` little-endian
637            /// bytes for binary formats.
638            #[inline]
639            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
640                if serializer.is_human_readable() {
641                    #[cfg(feature = "alloc")]
642                    {
643                        serializer.serialize_str(&self.0.to_string())
644                    }
645                    #[cfg(not(feature = "alloc"))]
646                    {
647                        let _ = serializer;
648                        Err(serde::ser::Error::custom(
649                            "decimal-scaled: human-readable serialisation requires `alloc`",
650                        ))
651                    }
652                } else {
653                    let mut bytes = [0u8; $bytes_len];
654                    let limbs = self.0.limbs_le();
655                    // limbs is [u64; $bytes_len / 8]; write 8 bytes
656                    // little-endian per limb. On LE targets this
657                    // produces the same byte sequence as the historic
658                    // [u128; $bytes_len / 16] path.
659                    for (i, limb) in limbs.iter().enumerate() {
660                        bytes[i * 8..(i + 1) * 8].copy_from_slice(&limb.to_le_bytes());
661                    }
662                    serializer.serialize_bytes(&bytes)
663                }
664            }
665        }
666
667        impl<'de, const SCALE: u32> Deserialize<'de> for $crate::types::widths::$Type<SCALE> {
668            #[inline]
669            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
670                struct V<const S: u32>;
671                impl<'de, const S: u32> Visitor<'de> for V<S> {
672                    type Value = $crate::types::widths::$Type<S>;
673                    fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
674                        formatter.write_str(concat!(
675                            "a base-10 integer string or ",
676                            stringify!($bytes_len),
677                            " little-endian bytes for ",
678                            stringify!($Type),
679                        ))
680                    }
681                    fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Self::Value, E> {
682                        let parsed = <$Storage>::from_str_radix(text, 10).map_err(|_| {
683                            serde::de::Error::custom(concat!(
684                                stringify!($Type),
685                                ": invalid base-10 integer string",
686                            ))
687                        })?;
688                        Ok(<$crate::types::widths::$Type<S>>::from_bits(parsed))
689                    }
690                    fn visit_borrowed_str<E: serde::de::Error>(
691                        self,
692                        text: &'de str,
693                    ) -> Result<Self::Value, E> {
694                        self.visit_str(text)
695                    }
696                    #[cfg(feature = "alloc")]
697                    fn visit_string<E: serde::de::Error>(
698                        self,
699                        text: alloc::string::String,
700                    ) -> Result<Self::Value, E> {
701                        self.visit_str(&text)
702                    }
703                    fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
704                        if bytes.len() != $bytes_len {
705                            return Err(serde::de::Error::invalid_length($bytes_len, &self));
706                        }
707                        // 8 bytes per u64 limb; bytes_len/8 limbs total.
708                        let mut limbs = [0u64; $bytes_len / 8];
709                        for (i, limb) in limbs.iter_mut().enumerate() {
710                            let mut buf = [0u8; 8];
711                            buf.copy_from_slice(&bytes[i * 8..(i + 1) * 8]);
712                            *limb = u64::from_le_bytes(buf);
713                        }
714                        Ok(<$crate::types::widths::$Type<S>>::from_bits(
715                            <$Storage>::from_limbs_le(limbs),
716                        ))
717                    }
718                    fn visit_borrowed_bytes<E: serde::de::Error>(
719                        self,
720                        bytes: &'de [u8],
721                    ) -> Result<Self::Value, E> {
722                        self.visit_bytes(bytes)
723                    }
724                }
725                if deserializer.is_human_readable() {
726                    deserializer.deserialize_str(V::<SCALE>)
727                } else {
728                    deserializer.deserialize_bytes(V::<SCALE>)
729                }
730            }
731        }
732    };
733}
734
735#[cfg(any(feature = "d57", feature = "wide"))]
736decl_wide_serde!(D57, crate::int::types::Int<3>, 24);
737#[cfg(any(feature = "d76", feature = "wide"))]
738decl_wide_serde!(D76, crate::int::types::Int<4>, 32);
739#[cfg(any(feature = "d115", feature = "wide"))]
740decl_wide_serde!(D115, crate::int::types::Int<6>, 48);
741#[cfg(any(feature = "d153", feature = "wide"))]
742decl_wide_serde!(D153, crate::int::types::Int<8>, 64);
743#[cfg(any(feature = "d230", feature = "wide"))]
744decl_wide_serde!(D230, crate::int::types::Int<12>, 96);
745#[cfg(any(feature = "d307", feature = "wide", feature = "x-wide"))]
746decl_wide_serde!(D307, crate::int::types::Int<16>, 128);
747#[cfg(any(feature = "d462", feature = "x-wide"))]
748decl_wide_serde!(D462, crate::int::types::Int<24>, 192);
749#[cfg(any(feature = "d616", feature = "x-wide"))]
750decl_wide_serde!(D616, crate::int::types::Int<32>, 256);
751#[cfg(any(feature = "d924", feature = "xx-wide"))]
752decl_wide_serde!(D924, crate::int::types::Int<48>, 384);
753#[cfg(any(feature = "d1232", feature = "xx-wide"))]
754decl_wide_serde!(D1232, crate::int::types::Int<64>, 512);
755
756#[cfg(all(test, feature = "wide"))]
757mod wide_serde_tests {
758
759    #[test]
760    fn d76_human_readable_round_trip() {
761        let value = crate::D::<crate::int::types::Int<4>, 12>::try_from(1_234_567_i128).unwrap();
762        let json = serde_json::to_string(&value).unwrap();
763        let back: crate::D<crate::int::types::Int<4>, 12> = serde_json::from_str(&json).unwrap();
764        assert_eq!(back, value);
765    }
766
767    #[test]
768    fn d76_negative_human_readable_round_trip() {
769        let value = -crate::D::<crate::int::types::Int<4>, 12>::try_from(987_654_321_i128).unwrap();
770        let json = serde_json::to_string(&value).unwrap();
771        let back: crate::D<crate::int::types::Int<4>, 12> = serde_json::from_str(&json).unwrap();
772        assert_eq!(back, value);
773    }
774
775    #[test]
776    fn d76_binary_round_trip() {
777        // postcard is a binary, non-self-describing format.
778        let value = crate::D::<crate::int::types::Int<4>, 12>::try_from(42_i128).unwrap();
779        let bytes = postcard::to_allocvec(&value).unwrap();
780        let back: crate::D<crate::int::types::Int<4>, 12> = postcard::from_bytes(&bytes).unwrap();
781        assert_eq!(back, value);
782    }
783}