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 `v` 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        v: &crate::D<crate::int::types::Int<2>, SCALE>,
148        s: S,
149    ) -> Result<S::Ok, S::Error> {
150        v.serialize(s)
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        d: D,
164    ) -> Result<crate::D<crate::int::types::Int<2>, SCALE>, D::Error> {
165        crate::D::<crate::int::types::Int<2>, SCALE>::deserialize(d)
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, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
187            f.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, v: &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 = v.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            v.parse::<i128>().map(|n| crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(n))).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, v: &'de str) -> Result<Self::Value, E> {
222            self.visit_str(v)
223        }
224
225        #[cfg(feature = "alloc")]
226        fn visit_string<E: serde::de::Error>(
227            self,
228            v: alloc::string::String,
229        ) -> Result<Self::Value, E> {
230            self.visit_str(&v)
231        }
232
233        // ── Bytes wire form (binary) ───────────────────────────────────
234
235        fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
236            // Require exactly 16 bytes: the little-endian i128 layout.
237            let arr: [u8; 16] = v.try_into().map_err(|_| {
238                serde::de::Error::invalid_length(
239                    v.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(arr))))
244        }
245
246        fn visit_borrowed_bytes<E: serde::de::Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
247            self.visit_bytes(v)
248        }
249
250        #[cfg(feature = "alloc")]
251        fn visit_byte_buf<E: serde::de::Error>(
252            self,
253            v: alloc::vec::Vec<u8>,
254        ) -> Result<Self::Value, E> {
255            self.visit_bytes(&v)
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, v: 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(v))))
268        }
269
270        fn visit_i16<E: serde::de::Error>(self, v: 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(v))))
272        }
273
274        fn visit_i32<E: serde::de::Error>(self, v: 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(v))))
276        }
277
278        fn visit_i64<E: serde::de::Error>(self, v: 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(v))))
280        }
281
282        fn visit_i128<E: serde::de::Error>(self, v: i128) -> Result<Self::Value, E> {
283            Ok(crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(v)))
284        }
285
286        fn visit_u8<E: serde::de::Error>(self, v: 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(v))))
288        }
289
290        fn visit_u16<E: serde::de::Error>(self, v: 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(v))))
292        }
293
294        fn visit_u32<E: serde::de::Error>(self, v: 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(v))))
296        }
297
298        fn visit_u64<E: serde::de::Error>(self, v: 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(v))))
300        }
301
302        fn visit_u128<E: serde::de::Error>(self, v: 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(v).map(|n| crate::D::<crate::int::types::Int<2>, SCALE>::from_bits(crate::int::types::Int::<2>::from_i128(n))).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 de: StrDeserializer<DeError> = "0".into_deserializer();
337        let v: D38s12 = D38s12::deserialize(de).unwrap();
338        assert_eq!(v, 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 v: D38s12 = <_ as Visitor>::visit_str::<DeError>(visitor, "1000000000000").unwrap();
347        assert_eq!(v, 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 res: Result<D38s12, _> = <_ as Visitor>::visit_str::<DeError>(visitor, "1.5");
356        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
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 v: D38s12 = <_ as Visitor>::visit_i64::<DeError>(visitor, -5).unwrap();
367        assert_eq!(v.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 v: D38s12 = <_ as Visitor>::visit_u64::<DeError>(visitor, u64::MAX).unwrap();
375        assert_eq!(v.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 res: Result<D38s12, _> =
384            <_ as Visitor>::visit_u128::<DeError>(visitor, (i128::MAX as u128) + 1);
385        assert!(res.is_err(), "expected overflow reject; got Ok({:?})", res);
386    }
387
388    // ── JSON round-trips ──────────────────────────────────────────────
389
390    /// `D38s12::ONE` serialises as the JSON string `"1000000000000"`.
391    /// This is the BigInt-compatible wire form, not the Display form
392    /// `"1.000000000000"`.
393    #[test]
394    fn json_one_serialises_as_scaled_integer_string() {
395        let json = serde_json::to_string(&D38s12::ONE).unwrap();
396        assert_eq!(json, "\"1000000000000\"");
397    }
398
399    #[test]
400    fn json_zero_serialises_as_zero_string() {
401        let json = serde_json::to_string(&D38s12::ZERO).unwrap();
402        assert_eq!(json, "\"0\"");
403    }
404
405    #[test]
406    fn json_one_round_trips() {
407        let json = serde_json::to_string(&D38s12::ONE).unwrap();
408        let back: D38s12 = serde_json::from_str(&json).unwrap();
409        assert_eq!(back, D38s12::ONE);
410    }
411
412    #[test]
413    fn json_zero_round_trips() {
414        let json = serde_json::to_string(&D38s12::ZERO).unwrap();
415        let back: D38s12 = serde_json::from_str(&json).unwrap();
416        assert_eq!(back, D38s12::ZERO);
417    }
418
419    /// Negative values round-trip through JSON. `try_from(-5_i32)` stores
420    /// `-5 * 10^12 = -5_000_000_000_000`.
421    #[test]
422    fn json_negative_round_trips() {
423        let v = D38s12::try_from(-5_i32).unwrap();
424        let json = serde_json::to_string(&v).unwrap();
425        assert_eq!(json, "\"-5000000000000\"");
426        let back: D38s12 = serde_json::from_str(&json).unwrap();
427        assert_eq!(back, v);
428        assert_eq!(back.to_bits(), -5_000_000_000_000_i128);
429    }
430
431    /// `D38::MAX` and `D38::MIN` round-trip exactly through the
432    /// JSON-string wire format.
433    #[test]
434    fn json_max_round_trips() {
435        let json = serde_json::to_string(&D38s12::MAX).unwrap();
436        let back: D38s12 = serde_json::from_str(&json).unwrap();
437        assert_eq!(back, D38s12::MAX);
438    }
439
440    #[test]
441    fn json_min_round_trips() {
442        let json = serde_json::to_string(&D38s12::MIN).unwrap();
443        let back: D38s12 = serde_json::from_str(&json).unwrap();
444        assert_eq!(back, D38s12::MIN);
445    }
446
447    /// The JSON string representation matches `i128::to_string` exactly.
448    /// On the JavaScript side, `BigInt(json).toString()` reproduces the
449    /// same digits.
450    #[test]
451    fn json_string_matches_i128_to_string() {
452        let raw: i128 = -123_456_789_012_345_678_901_234_567_890_i128;
453        let v = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(raw));
454        let json = serde_json::to_string(&v).unwrap();
455        assert_eq!(json, format!("\"{}\"", raw));
456    }
457
458    // ── JSON: malformed input rejection ───────────────────────────────
459
460    #[test]
461    fn json_rejects_decimal_point_string() {
462        let res: Result<D38s12, _> = serde_json::from_str("\"1.5\"");
463        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
464    }
465
466    #[test]
467    fn json_rejects_scientific_notation_string() {
468        let res: Result<D38s12, _> = serde_json::from_str("\"1e6\"");
469        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
470    }
471
472    #[test]
473    fn json_rejects_not_a_number_string() {
474        let res: Result<D38s12, _> = serde_json::from_str("\"not-a-number\"");
475        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
476    }
477
478    #[test]
479    fn json_rejects_empty_string() {
480        let res: Result<D38s12, _> = serde_json::from_str("\"\"");
481        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
482    }
483
484    #[test]
485    fn json_rejects_leading_whitespace_string() {
486        // `i128::from_str` does not trim whitespace; the wire format
487        // requires a strict integer literal.
488        let res: Result<D38s12, _> = serde_json::from_str("\"  42\"");
489        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
490    }
491
492    #[test]
493    fn json_rejects_plus_prefix() {
494        let res: Result<D38s12, _> = serde_json::from_str("\"+42\"");
495        assert!(res.is_err(), "expected reject; got Ok({:?})", res);
496    }
497
498    /// A bare JSON integer (not a string) is accepted via `visit_i64`.
499    /// The number is interpreted as the scaled storage value.
500    #[test]
501    fn json_accepts_bare_integer_number_as_storage() {
502        let back: D38s12 = serde_json::from_str("42").unwrap();
503        assert_eq!(back.to_bits(), 42_i128);
504    }
505
506    // ── Postcard binary 16-byte LE round-trips ────────────────────────
507
508    #[test]
509    fn postcard_one_round_trips() {
510        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::ONE).unwrap();
511        // Verify the raw 16 LE bytes appear somewhere in the postcard
512        // output (postcard may prepend a varint length prefix).
513        let raw = D38s12::ONE.to_bits().as_i128().to_le_bytes();
514        assert!(bytes.windows(16).any(|w| w == raw));
515        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
516        assert_eq!(back, D38s12::ONE);
517    }
518
519    #[test]
520    fn postcard_zero_round_trips() {
521        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::ZERO).unwrap();
522        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
523        assert_eq!(back, D38s12::ZERO);
524    }
525
526    #[test]
527    fn postcard_negative_round_trips() {
528        let v = D38s12::try_from(-5_i32).unwrap();
529        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&v).unwrap();
530        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
531        assert_eq!(back, v);
532    }
533
534    #[test]
535    fn postcard_max_round_trips() {
536        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::MAX).unwrap();
537        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
538        assert_eq!(back, D38s12::MAX);
539    }
540
541    #[test]
542    fn postcard_min_round_trips() {
543        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&D38s12::MIN).unwrap();
544        let back: D38s12 = postcard::from_bytes(&bytes).unwrap();
545        assert_eq!(back, D38s12::MIN);
546    }
547
548    /// The postcard payload contains the raw `i128::to_le_bytes`
549    /// representation. The first LE byte is the LSB and the last is
550    /// the MSB.
551    #[test]
552    fn postcard_byte_order_matches_le() {
553        let v = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(0x0123_4567_89AB_CDEF_FEDC_BA98_7654_3210_i128));
554        let bytes: alloc::vec::Vec<u8> = postcard::to_allocvec(&v).unwrap();
555        let raw = v.to_bits().as_i128().to_le_bytes();
556        let found = bytes.windows(16).position(|w| w == raw);
557        assert!(
558            found.is_some(),
559            "expected raw LE bytes embedded; got {:?}",
560            bytes
561        );
562        assert_eq!(raw[0], 0x10); // LSB of the i128
563        assert_eq!(raw[15], 0x01); // MSB of the i128
564    }
565
566    // ── Cross-format compatibility ─────────────────────────────────────
567
568    /// The JSON integer string, when parsed back to `i128` and converted
569    /// to `to_le_bytes`, matches the binary wire representation directly.
570    #[test]
571    fn cross_format_json_string_matches_le_bytes() {
572        let v = D38s12::try_from(42_i32).unwrap();
573        let json = serde_json::to_string(&v).unwrap();
574        let inner = json.trim_matches('"');
575        let parsed: i128 = inner.parse().unwrap();
576        let json_bytes = parsed.to_le_bytes();
577        let direct_bytes = v.to_bits().as_i128().to_le_bytes();
578        assert_eq!(json_bytes, direct_bytes);
579    }
580
581    /// Different SCALE values serialise identically when they share the
582    /// same raw storage. The SCALE is a compile-time type parameter and
583    /// is not encoded in the wire.
584    #[test]
585    fn cross_scale_wire_is_storage_only() {
586        let raw: i128 = 1_500_000_000_000;
587        let v12 = crate::D::<crate::int::types::Int<2>, 12>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
588        let v6 = crate::D::<crate::int::types::Int<2>, 6>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
589        assert_eq!(serde_json::to_string(&v12).unwrap(), "\"1500000000000\"");
590        assert_eq!(serde_json::to_string(&v6).unwrap(), "\"1500000000000\"");
591    }
592
593    // ── decimal_serde free-function helpers ───────────────────────────
594
595    /// The `#[serde(with = "...")]` helpers delegate to the inherent
596    /// impls and produce the correct JSON output.
597    #[test]
598    fn decimal_serde_helper_round_trips() {
599        #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
600        struct Holder {
601            #[serde(with = "crate::serde_helpers::decimal_serde")]
602            length: crate::D<crate::int::types::Int<2>, 12>,
603        }
604
605        let h = Holder {
606            length: D38s12::try_from(7_i32).unwrap(),
607        };
608        let json = serde_json::to_string(&h).unwrap();
609        assert_eq!(json, r#"{"length":"7000000000000"}"#);
610        let back: Holder = serde_json::from_str(&json).unwrap();
611        assert_eq!(back, h);
612    }
613}
614
615// ─── Wide-tier serde (D57 through D1232) ────────────────────────────
616//
617// The wide-tier wire format mirrors D38's: a base-10 integer string
618// of the raw storage value for human-readable serializers, and the
619// raw little-endian limb bytes for binary serializers. The
620// implementation is intentionally slimmer than D38's — no
621// native-integer visit methods, since no native int can losslessly
622// carry the >128-bit storage anyway.
623
624/// Emits `Serialize` / `Deserialize` for a wide-tier decimal type
625/// (D57 through D1232). `$bytes_len` is `mem::size_of::<$Storage>()`
626/// (e.g. 32 for `Int<4>`).
627#[cfg(feature = "_wide-support")]
628macro_rules! decl_wide_serde {
629    ($Type:ident, $Storage:ty, $bytes_len:literal) => {
630        impl<const SCALE: u32> Serialize for $crate::types::widths::$Type<SCALE> {
631            /// Serialise as a base-10 integer string for human-
632            /// readable formats, or as `$bytes_len` little-endian
633            /// bytes for binary formats.
634            #[inline]
635            fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
636                if s.is_human_readable() {
637                    #[cfg(feature = "alloc")]
638                    {
639                        s.serialize_str(&self.0.to_string())
640                    }
641                    #[cfg(not(feature = "alloc"))]
642                    {
643                        let _ = s;
644                        Err(serde::ser::Error::custom(
645                            "decimal-scaled: human-readable serialisation requires `alloc`",
646                        ))
647                    }
648                } else {
649                    let mut bytes = [0u8; $bytes_len];
650                    let limbs = self.0.limbs_le();
651                    // limbs is [u64; $bytes_len / 8]; write 8 bytes
652                    // little-endian per limb. On LE targets this
653                    // produces the same byte sequence as the historic
654                    // [u128; $bytes_len / 16] path.
655                    for (i, limb) in limbs.iter().enumerate() {
656                        bytes[i * 8..(i + 1) * 8].copy_from_slice(&limb.to_le_bytes());
657                    }
658                    s.serialize_bytes(&bytes)
659                }
660            }
661        }
662
663        impl<'de, const SCALE: u32> Deserialize<'de> for $crate::types::widths::$Type<SCALE> {
664            #[inline]
665            fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
666                struct V<const S: u32>;
667                impl<'de, const S: u32> Visitor<'de> for V<S> {
668                    type Value = $crate::types::widths::$Type<S>;
669                    fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
670                        f.write_str(concat!(
671                            "a base-10 integer string or ",
672                            stringify!($bytes_len),
673                            " little-endian bytes for ",
674                            stringify!($Type),
675                        ))
676                    }
677                    fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
678                        let parsed = <$Storage>::from_str_radix(v, 10).map_err(|_| {
679                            serde::de::Error::custom(concat!(
680                                stringify!($Type),
681                                ": invalid base-10 integer string",
682                            ))
683                        })?;
684                        Ok(<$crate::types::widths::$Type<S>>::from_bits(parsed))
685                    }
686                    fn visit_borrowed_str<E: serde::de::Error>(
687                        self,
688                        v: &'de str,
689                    ) -> Result<Self::Value, E> {
690                        self.visit_str(v)
691                    }
692                    #[cfg(feature = "alloc")]
693                    fn visit_string<E: serde::de::Error>(
694                        self,
695                        v: alloc::string::String,
696                    ) -> Result<Self::Value, E> {
697                        self.visit_str(&v)
698                    }
699                    fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
700                        if v.len() != $bytes_len {
701                            return Err(serde::de::Error::invalid_length($bytes_len, &self));
702                        }
703                        // 8 bytes per u64 limb; bytes_len/8 limbs total.
704                        let mut limbs = [0u64; $bytes_len / 8];
705                        for (i, limb) in limbs.iter_mut().enumerate() {
706                            let mut buf = [0u8; 8];
707                            buf.copy_from_slice(&v[i * 8..(i + 1) * 8]);
708                            *limb = u64::from_le_bytes(buf);
709                        }
710                        Ok(<$crate::types::widths::$Type<S>>::from_bits(
711                            <$Storage>::from_limbs_le(limbs),
712                        ))
713                    }
714                    fn visit_borrowed_bytes<E: serde::de::Error>(
715                        self,
716                        v: &'de [u8],
717                    ) -> Result<Self::Value, E> {
718                        self.visit_bytes(v)
719                    }
720                }
721                if d.is_human_readable() {
722                    d.deserialize_str(V::<SCALE>)
723                } else {
724                    d.deserialize_bytes(V::<SCALE>)
725                }
726            }
727        }
728    };
729}
730
731#[cfg(any(feature = "d57", feature = "wide"))]
732decl_wide_serde!(D57, crate::int::types::Int<3>, 24);
733#[cfg(any(feature = "d76", feature = "wide"))]
734decl_wide_serde!(D76, crate::int::types::Int<4>, 32);
735#[cfg(any(feature = "d115", feature = "wide"))]
736decl_wide_serde!(D115, crate::int::types::Int<6>, 48);
737#[cfg(any(feature = "d153", feature = "wide"))]
738decl_wide_serde!(D153, crate::int::types::Int<8>, 64);
739#[cfg(any(feature = "d230", feature = "wide"))]
740decl_wide_serde!(D230, crate::int::types::Int<12>, 96);
741#[cfg(any(feature = "d307", feature = "wide", feature = "x-wide"))]
742decl_wide_serde!(D307, crate::int::types::Int<16>, 128);
743#[cfg(any(feature = "d462", feature = "x-wide"))]
744decl_wide_serde!(D462, crate::int::types::Int<24>, 192);
745#[cfg(any(feature = "d616", feature = "x-wide"))]
746decl_wide_serde!(D616, crate::int::types::Int<32>, 256);
747#[cfg(any(feature = "d924", feature = "xx-wide"))]
748decl_wide_serde!(D924, crate::int::types::Int<48>, 384);
749#[cfg(any(feature = "d1232", feature = "xx-wide"))]
750decl_wide_serde!(D1232, crate::int::types::Int<64>, 512);
751
752#[cfg(all(test, feature = "wide"))]
753mod wide_serde_tests {
754
755    #[test]
756    fn d76_human_readable_round_trip() {
757        let v = crate::D::<crate::int::types::Int<4>, 12>::try_from(1_234_567_i128).unwrap();
758        let json = serde_json::to_string(&v).unwrap();
759        let back: crate::D<crate::int::types::Int<4>, 12> = serde_json::from_str(&json).unwrap();
760        assert_eq!(back, v);
761    }
762
763    #[test]
764    fn d76_negative_human_readable_round_trip() {
765        let v = -crate::D::<crate::int::types::Int<4>, 12>::try_from(987_654_321_i128).unwrap();
766        let json = serde_json::to_string(&v).unwrap();
767        let back: crate::D<crate::int::types::Int<4>, 12> = serde_json::from_str(&json).unwrap();
768        assert_eq!(back, v);
769    }
770
771    #[test]
772    fn d76_binary_round_trip() {
773        // postcard is a binary, non-self-describing format.
774        let v = crate::D::<crate::int::types::Int<4>, 12>::try_from(42_i128).unwrap();
775        let bytes = postcard::to_allocvec(&v).unwrap();
776        let back: crate::D<crate::int::types::Int<4>, 12> = postcard::from_bytes(&bytes).unwrap();
777        assert_eq!(back, v);
778    }
779}