msgpack_tagged/
serializer.rs

1//! Tagged-map msgpack serializer that wraps [`rmp_serde::Serializer`].
2//!
3//! Most of `serde::Serializer`'s methods are forwarded to the inner
4//! `rmp_serde` serializer unchanged — we only intercept the structurally
5//! significant calls (`serialize_struct`, the variant methods, etc.) and
6//! re-emit them as integer-keyed msgpack maps using the [`TagRegistry`].
7//!
8//! The public entry point is [`msgpack_tagged_serialize`], which builds the
9//! registry up front via `T::register_into` and runs the value through the
10//! wrapper. The wrapper in turn writes to a `Vec<u8>` we hand back to the
11//! caller.
12//!
13//! Every aggregate shape is intercepted: named structs, multi-element tuple
14//! structs, sequences, fixed-length tuples, free-form maps, and all four
15//! variant kinds (unit / newtype / tuple / struct). Primitives, top-level
16//! newtype structs, and `Option` forward through to inner — recursing into
17//! nested values via this same wrapper so any tagged value reachable from
18//! the root keeps the int-keyed-map treatment.
19//!
20//! ## Known gaps vs. the design doc / macro syntax
21//!
22//! The wrapper isn't final — the bits below are accepted by
23//! `#[derive(MsgpackTagged)]` today but aren't reflected in the wire bytes
24//! we produce yet.
25//!
26//! - **Tag-ascending wire order.** The design promises field/element
27//!   entries on the wire in tag-ascending order so two semantically-equal
28//!   values encode byte-identically regardless of source-declaration
29//!   order. We currently emit in serde's call-order = source-declaration
30//!   order. Tightening this requires buffering field bytes before writing.
31//! - **Encoding strategies.** Only the **Tagged** strategy (int-keyed
32//!   map) is implemented. Per-type strategy overrides — **Array**
33//!   (positional msgpack array, smallest wire) and **Named** (rmp_serde
34//!   default, string-keyed map) — are deferred follow-ups.
35//! - **`assert_eq!` on `len` vs `product.fields.len()`** — already
36//!   tightened, but only inside the four product-shaped methods. New
37//!   shapes that join the family should add the same assert.
38
39use std::collections::HashMap;
40use std::io::Write;
41
42use rmp_serde::Serializer as RmpSerializer;
43// `ser::Serializer` would clash with our own `Serializer` struct below if
44// pulled in via `use`; importing the `ser` module instead lets us write
45// `ser::Serializer` for the trait at the few sites that need it.
46use serde::ser::{
47    self, Error as _, Serialize, SerializeMap, SerializeSeq, SerializeStruct,
48    SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant,
49};
50
51use crate::{EncodingStrategy, MsgpackTagged, TagRegistry, type_name_basename};
52
53/// Tagged-map msgpack serializer.
54///
55/// Borrows a caller-owned [`TagRegistry`] and carries per-encode-session
56/// policy (default strategy + per-type overrides). The registry stays pure
57/// type metadata — strategy state lives only on the serializer. Typical
58/// usage:
59///
60/// ```ignore
61/// let registry = TagRegistry::from_type::<Program<F>>();
62/// let mut buf = Vec::new();
63/// let mut s = msgpack_tagged::Serializer::new(&mut buf, &registry)
64///     .with_default_strategy(EncodingStrategy::Array)
65///     .with_strategy::<Program<F>>(EncodingStrategy::Tagged)
66///     .with_strategy::<Circuit<F>>(EncodingStrategy::Tagged);
67/// program.serialize(&mut s)?;
68/// ```
69///
70/// `new` defaults the strategy to [`EncodingStrategy::Tagged`] (the most
71/// evolution-friendly shape); `with_default_strategy` swaps it, and
72/// `with_strategy::<U>` overrides for individual types (panicking on a
73/// registry miss). Policy sites that target types by string name —
74/// possibly without knowing whether the type is reachable — use
75/// `with_strategy_for_name` instead.
76pub struct Serializer<'a, W: Write> {
77    inner: RmpSerializer<W>,
78    registry: &'a TagRegistry,
79    default_strategy: EncodingStrategy,
80    /// Per-type strategy overrides keyed by **serde name** (the name
81    /// `#[serde(rename = "...")]` resolves to, or the bare type ident when
82    /// no rename is set). Keying by name — not [`std::any::TypeId`] — lets
83    /// `with_strategy::<Foo<F>>` apply uniformly across field flavors
84    /// (`Foo<FieldElement>` and `Foo<OtherF>` share the name "Foo") and
85    /// across shadow DTOs (a public `Circuit<F>` with
86    /// `#[tagged(via(CircuitWire<F>))]` reaches the same "Circuit"
87    /// override that CircuitWire registers under via `#[serde(rename)]`).
88    /// See [`type_name_basename`] for the derivation rule.
89    overrides: HashMap<&'static str, EncodingStrategy>,
90}
91
92impl<'a, W: Write> Serializer<'a, W> {
93    /// Construct a serializer over `writer` borrowing the caller-built
94    /// `registry`. The default per-type strategy is
95    /// [`EncodingStrategy::Tagged`].
96    ///
97    /// Build the registry up front via `TagRegistry::from_type::<T>()` for
98    /// the top-level type being serialized — the registration walk seeds
99    /// every nested tagged type.
100    pub fn new(writer: W, registry: &'a TagRegistry) -> Self {
101        // Tagged types' `serialize_struct` / variant calls route
102        // through our interception layer below; the inner
103        // `RmpSerializer` only gets the structurally-irrelevant calls
104        // (primitives, `serialize_bytes`, etc.). See
105        // [`make_inner_rmp_serializer`] for why no `BytesMode`
106        // override is applied (and the rule for new bytes-shaped
107        // types: call `serialize_bytes` directly in their
108        // `Serialize` impl).
109        Self {
110            inner: make_inner_rmp_serializer(writer),
111            registry,
112            default_strategy: EncodingStrategy::Tagged,
113            overrides: HashMap::new(),
114        }
115    }
116
117    /// Change the default strategy applied to types without a per-type
118    /// override.
119    pub fn with_default_strategy(mut self, strategy: EncodingStrategy) -> Self {
120        self.default_strategy = strategy;
121        self
122    }
123
124    /// Set the encoding strategy for a specific type `T`. Overrides the
125    /// default for this type only; later calls for the same `T` win.
126    /// Resolves `T`'s serde name via [`type_name_basename`] and inserts
127    /// the override under that name.
128    ///
129    /// **Panics** if `T`'s name isn't in the registry — setting a
130    /// strategy for an unreachable type is almost always a bug (the
131    /// override would silently never fire). Use this at call sites where
132    /// the override targets a type the caller *knows* should be
133    /// reachable (typically tests, or a producer that just registered
134    /// the type up front). Policy sites that configure overrides for a
135    /// fixed catalog of potentially-top-level types — where the caller's
136    /// `T` might not reach all of them — should use
137    /// [`Self::with_strategy_for_name`] instead.
138    pub fn with_strategy<T: MsgpackTagged>(mut self, strategy: EncodingStrategy) -> Self {
139        let name = type_name_basename::<T>();
140        assert!(
141            self.registry.contains(name),
142            "Serializer::with_strategy: serde name {name:?} (from type {full_name}) is \
143             not in the registry — the top-level type's `register_into` walk didn't \
144             reach it. Build the registry from a type that transitively visits T, use \
145             `with_strategy_for_name` for policy sites that tolerate unreachable names, \
146             or remove the override.",
147            full_name = std::any::type_name::<T>(),
148        );
149        self.overrides.insert(name, strategy);
150        self
151    }
152
153    /// Set the encoding strategy for the type registered under serde
154    /// `name`. Never asserts — names not in the registry get a stray
155    /// override entry that's never looked up at encode time (harmless).
156    /// Sibling of [`Self::with_strategy`] for *policy* sites that
157    /// configure overrides for a fixed catalog of potentially-top-level
158    /// types but don't know which ones the caller will actually
159    /// serialize.
160    ///
161    /// For tests or producer call sites that want to fail fast on a
162    /// registry miss, use [`Self::with_strategy`] instead.
163    pub fn with_strategy_for_name(
164        mut self,
165        name: &'static str,
166        strategy: EncodingStrategy,
167    ) -> Self {
168        self.overrides.insert(name, strategy);
169        self
170    }
171
172    /// Look up the effective encoding strategy for the registered type
173    /// `T`. Returns the per-type override if one was set; otherwise the
174    /// default strategy. Used by the encode-time dispatch and exposed for
175    /// tests.
176    pub fn strategy_for<T: MsgpackTagged>(&self) -> EncodingStrategy {
177        self.strategy_for_name(type_name_basename::<T>())
178    }
179}
180
181/// Build the tag registry from `T::register_into`, then serialize `value`
182/// through a [`Serializer`] into a freshly-allocated `Vec<u8>`. Uses the
183/// default [`EncodingStrategy::Tagged`] for all types. For strategy
184/// customization, build the registry and serializer directly and use the
185/// builder methods.
186pub fn msgpack_tagged_serialize<T>(value: &T) -> std::io::Result<Vec<u8>>
187where
188    T: ?Sized + Serialize + MsgpackTagged,
189{
190    let registry = TagRegistry::from_type::<T>();
191    let mut buf = Vec::new();
192    let mut serializer = Serializer::new(&mut buf, &registry);
193    value.serialize(&mut serializer).map_err(std::io::Error::other)?;
194    Ok(buf)
195}
196
197/// `rmp_serde`'s error type, re-exported for our `serde::Serializer` impl.
198type RmpError = rmp_serde::encode::Error;
199
200// ============================================================================
201// `serde::Serializer` impl — most methods forward to the inner rmp_serde
202// serializer; the structurally-significant ones (struct, variants, tuple
203// shapes) are intercepted to emit int-keyed maps via the registry.
204// ============================================================================
205
206impl<'ser, 'a, W: Write> ser::Serializer for &'ser mut Serializer<'a, W> {
207    type Ok = ();
208    type Error = RmpError;
209
210    type SerializeSeq = TaggedSerializeViaParent<'ser, 'a, W>;
211    type SerializeTuple = TaggedSerializeViaParent<'ser, 'a, W>;
212    type SerializeTupleStruct = TaggedSerializeProduct<'ser, 'a, W>;
213    type SerializeTupleVariant = TaggedSerializeProduct<'ser, 'a, W>;
214    type SerializeMap = TaggedSerializeViaParent<'ser, 'a, W>;
215    type SerializeStruct = TaggedSerializeProduct<'ser, 'a, W>;
216    type SerializeStructVariant = TaggedSerializeProduct<'ser, 'a, W>;
217
218    // -------- primitive scalars: forward verbatim ---------------------------
219
220    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
221        self.inner.serialize_bool(v)
222    }
223
224    fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
225        self.inner.serialize_i8(v)
226    }
227    fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> {
228        self.inner.serialize_i16(v)
229    }
230    fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> {
231        self.inner.serialize_i32(v)
232    }
233    fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
234        self.inner.serialize_i64(v)
235    }
236    fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
237        self.inner.serialize_i128(v)
238    }
239
240    fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
241        self.inner.serialize_u8(v)
242    }
243    fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> {
244        self.inner.serialize_u16(v)
245    }
246    fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
247        self.inner.serialize_u32(v)
248    }
249    fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
250        self.inner.serialize_u64(v)
251    }
252    fn serialize_u128(self, v: u128) -> Result<Self::Ok, Self::Error> {
253        self.inner.serialize_u128(v)
254    }
255
256    fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> {
257        self.inner.serialize_f32(v)
258    }
259    fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> {
260        self.inner.serialize_f64(v)
261    }
262
263    fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
264        self.inner.serialize_char(v)
265    }
266    fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
267        self.inner.serialize_str(v)
268    }
269    fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
270        self.inner.serialize_bytes(v)
271    }
272
273    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
274        self.inner.serialize_none()
275    }
276    fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
277    where
278        T: ?Sized + Serialize,
279    {
280        // Re-route `Some(inner)` through ourselves so nested tagged types
281        // get the int-keyed treatment too.
282        value.serialize(self)
283    }
284
285    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
286        self.inner.serialize_unit()
287    }
288
289    fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
290        // No fields → no tag table to consult; passthrough is fine.
291        self.inner.serialize_unit_struct(name)
292    }
293
294    fn serialize_newtype_struct<T>(
295        self,
296        _name: &'static str,
297        value: &T,
298    ) -> Result<Self::Ok, Self::Error>
299    where
300        T: ?Sized + Serialize,
301    {
302        // Newtype structs pass through to the inner type — that's the
303        // language-level convention we mirrored in the macro (`expand_newtype`
304        // emits an empty `Tagged::Product`). Re-route through ourselves so
305        // the inner type gets the tagged treatment if applicable.
306        value.serialize(self)
307    }
308
309    // -------- collection / map shapes: intercepted -------------------------
310    //
311    // We write the array/map header directly to the underlying writer and
312    // route each element/entry back through *this* wrapper via dedicated
313    // adapters (`TaggedSerializeArray`, `TaggedSerializeMap`). Without this
314    // interception, rmp_serde's adapters would route nested values through
315    // its own inner serializer — a tagged element inside a `Vec<Tagged>` /
316    // `BTreeMap<_, Tagged>` would then fall through to rmp's default
317    // positional-array struct encoding instead of recursing back to our
318    // int-keyed map shape.
319
320    fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
321        // msgpack arrays are length-prefixed, so we need a known length up
322        // front — same constraint rmp_serde itself imposes.
323        let len = len.ok_or_else(|| {
324            RmpError::custom("MsgpackTagged: sequences need a known length to encode")
325        })?;
326        write_array_header(self.inner.get_mut(), len)?;
327        Ok(TaggedSerializeViaParent { parent: self })
328    }
329
330    fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
331        write_array_header(self.inner.get_mut(), len)?;
332        Ok(TaggedSerializeViaParent { parent: self })
333    }
334
335    fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
336        let len = len
337            .ok_or_else(|| RmpError::custom("MsgpackTagged: maps need a known length to encode"))?;
338        write_map_header(self.inner.get_mut(), len)?;
339        Ok(TaggedSerializeViaParent { parent: self })
340    }
341
342    // -------- product shapes (named struct + multi-element tuple struct) ---
343
344    fn serialize_tuple_struct(
345        self,
346        name: &'static str,
347        len: usize,
348    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
349        // Same on-wire shape as a named struct — the registered `Product`
350        // uses positional wire-names ("0", "1", …) instead of field idents.
351        // Both shapes share the same adapter; the trait impl that fires is
352        // chosen by serde based on which `serialize_*` call landed.
353        self.begin_product(name, len)
354    }
355
356    fn serialize_struct(
357        self,
358        name: &'static str,
359        len: usize,
360    ) -> Result<Self::SerializeStruct, Self::Error> {
361        self.begin_product(name, len)
362    }
363
364    // -------- sum shapes: every variant becomes `{<variant_tag>: <payload>}`,
365    // with the payload shape determined by `VariantKind`. Each branch looks
366    // up the `Sum`, resolves the variant by name, writes a 1-entry outer map
367    // header (variant tag → payload), then emits the payload appropriate to
368    // the kind: `nil` for unit, the inner value pass-through for newtype, or
369    // a `Product`-shaped payload for tuple/struct (whose shape follows the
370    // enclosing enum's strategy).
371
372    fn serialize_unit_variant(
373        self,
374        name: &'static str,
375        _variant_index: u32,
376        variant: &'static str,
377    ) -> Result<Self::Ok, Self::Error> {
378        self.write_variant_header(name, variant)?;
379        ser::Serializer::serialize_unit(&mut *self)
380    }
381
382    fn serialize_newtype_variant<T>(
383        self,
384        name: &'static str,
385        _variant_index: u32,
386        variant: &'static str,
387        value: &T,
388    ) -> Result<Self::Ok, Self::Error>
389    where
390        T: ?Sized + Serialize,
391    {
392        self.write_variant_header(name, variant)?;
393        // Pass-through: inner bytes go directly under the variant tag, with no
394        // payload field-tag wrapping. Routing through `&mut *self` keeps any
395        // nested tagged types inside `value` recursing through this wrapper.
396        value.serialize(&mut *self)
397    }
398
399    fn serialize_tuple_variant(
400        self,
401        name: &'static str,
402        _variant_index: u32,
403        variant: &'static str,
404        len: usize,
405    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
406        self.begin_variant_payload(name, variant, len)
407    }
408
409    fn serialize_struct_variant(
410        self,
411        name: &'static str,
412        _variant_index: u32,
413        variant: &'static str,
414        len: usize,
415    ) -> Result<Self::SerializeStructVariant, Self::Error> {
416        self.begin_variant_payload(name, variant, len)
417    }
418}
419
420impl<'a, W: Write> Serializer<'a, W> {
421    /// Shared body of `serialize_struct` / `serialize_tuple_struct`.
422    /// Resolves the product + strategy, writes the strategy-appropriate
423    /// resolves the product + strategy, returns a configured
424    /// `TaggedSerializeProduct`. The two trait methods are identical at
425    /// this level — only the trait impl fired on the returned adapter
426    /// differs (which serde picks based on the caller's `serialize_*`
427    /// choice).
428    ///
429    /// **Buffering policy.** Per-field bytes are buffered into the
430    /// adapter and flushed in tag-ascending order at `end()` whenever
431    /// the user's source-declaration order has been deliberately
432    /// reordered relative to the tags
433    /// (`!product.tag_order_matches_source`). This is what makes both
434    /// strategies emit canonical (tag-ascending) wire order:
435    ///
436    /// * Under `Array` it's a *correctness* requirement — the decoder
437    ///   reads positionally and would otherwise see fields in the wrong
438    ///   slots.
439    /// * Under `Tagged` it's a *byte-determinism* requirement — the
440    ///   decoder doesn't care about order (every wire entry carries its
441    ///   tag), but consumers reading the bytes do: cross-implementation
442    ///   compatibility, hashing, cryptographic commitments. The design
443    ///   doc's "TAGS define the canonical field order" promise applies
444    ///   here.
445    ///
446    /// Types whose source-declaration order is already tag-ascending
447    /// (the common case — newly-added types and types using implicit
448    /// positional tags) skip the buffer entirely: the outer header is
449    /// written upfront and each field streams through to the parent
450    /// directly, saving the per-field `Vec<u8>` allocation. The cost is
451    /// paid only by types whose tags have drifted out of source order
452    /// — typically schema-evolved types with retired-and-re-added
453    /// fields. **If you reorder fields, you're opting into a per-field
454    /// allocation at encode time.**
455    fn begin_product<'ser>(
456        &'ser mut self,
457        name: &'static str,
458        len: usize,
459    ) -> Result<TaggedSerializeProduct<'ser, 'a, W>, RmpError> {
460        let (product, strategy) = self.product_and_strategy_for(name);
461        assert_field_count_matches(name, len, product.fields.len());
462        let strategy = downgrade_array_if_unsafe(&product, strategy);
463        begin_product_payload(self, product, strategy)
464    }
465
466    /// Shared body of `serialize_tuple_variant` / `serialize_struct_variant`.
467    /// Writes the outer 1-entry `{variant_tag: ...}` discriminator map
468    /// (always Tagged — variant identification is by integer tag under
469    /// `MsgpackTagged`); same buffering policy as `begin_product` for the
470    /// payload itself.
471    fn begin_variant_payload<'ser>(
472        &'ser mut self,
473        name: &'static str,
474        variant: &'static str,
475        len: usize,
476    ) -> Result<TaggedSerializeProduct<'ser, 'a, W>, RmpError> {
477        let v = self.write_variant_header(name, variant)?;
478        assert_field_count_matches(variant, len, v.payload.fields.len());
479        let strategy = self.strategy_for_name(name);
480        let strategy = downgrade_array_if_unsafe(&v.payload, strategy);
481        begin_product_payload(self, v.payload, strategy)
482    }
483
484    /// Spawn a sub-serializer over a fresh `Vec<u8>` buffer, inheriting
485    /// this serializer's registry + per-type strategy config. Used by
486    /// [`TaggedSerializeProduct::serialize_tag_and_value`] to encode each
487    /// field's bytes into a temp buffer before flushing in tag-ascending
488    /// order at `end()`. Cloning the `overrides` map is cheap in practice
489    /// — it's tiny (a few entries) and only walked per top-level value's
490    /// field count.
491    fn sub_serializer_into<'sub>(
492        &self,
493        writer: &'sub mut Vec<u8>,
494    ) -> Serializer<'a, &'sub mut Vec<u8>> {
495        Serializer {
496            inner: make_inner_rmp_serializer(writer),
497            registry: self.registry,
498            default_strategy: self.default_strategy,
499            overrides: self.overrides.clone(),
500        }
501    }
502
503    /// Resolve a registered `Product` by serde name. Used by both
504    /// `serialize_struct` and `serialize_tuple_struct`. A registry miss or a
505    /// sum-shaped entry signals a real bug — `register_into` should have
506    /// reached every type encoded under our wrapper, and the macro guarantees
507    /// product/sum shape matches the Rust definition — so we panic loudly per
508    /// the design doc rather than fabricating a synthetic shape.
509    fn product_for(&self, name: &'static str) -> crate::Product {
510        let entry = self.registry.get(name).unwrap_or_else(|| {
511            panic!(
512                "MsgpackTagged registry miss for {name:?} — the top-level `register_into` \
513                 walk should have registered every reachable type. Either the type is \
514                 missing `#[derive(MsgpackTagged)]` (or a hand-written impl that calls \
515                 `try_insert`), or its `serde` name doesn't match the registered name \
516                 (check `#[serde(rename = ...)]`)"
517            )
518        });
519        entry.tagged().as_product().unwrap_or_else(|| {
520            panic!("registry entry for {name:?} is sum-shaped but a product shape was expected")
521        })
522    }
523
524    /// Resolve a registered `Product` *and* the effective encoding strategy
525    /// for the type registered under `name`. Used only on the top-level
526    /// struct paths — variant payloads are forced Tagged at their
527    /// construction sites.
528    fn product_and_strategy_for(&self, name: &'static str) -> (crate::Product, EncodingStrategy) {
529        (self.product_for(name), self.strategy_for_name(name))
530    }
531
532    /// Resolve the effective encoding strategy for the type registered
533    /// under serde `name`. Overrides are keyed by the same serde-name
534    /// string the caller passes here, so it's a single hash lookup with
535    /// no registry indirection. Absent any override the per-serializer
536    /// `default_strategy` applies.
537    fn strategy_for_name(&self, name: &str) -> EncodingStrategy {
538        self.overrides.get(name).copied().unwrap_or(self.default_strategy)
539    }
540
541    /// Resolve a registered `Variant` by enum-type name + variant name. Used
542    /// by all four `serialize_*_variant` methods. A registry miss, a
543    /// product-shaped entry, or an unknown variant name signals a real bug —
544    /// the macro and serde-derive should agree on which name lives where.
545    fn variant_for(&self, name: &'static str, variant_name: &'static str) -> crate::Variant {
546        let entry = self.registry.get(name).unwrap_or_else(|| {
547            panic!(
548                "MsgpackTagged registry miss for enum {name:?} — the top-level \
549                 `register_into` walk should have registered every reachable type"
550            )
551        });
552        let sum = entry.tagged().as_sum().unwrap_or_else(|| {
553            panic!(
554                "registry entry for {name:?} is product-shaped but a sum shape was expected \
555                 (a `serialize_*_variant` call landed here)"
556            )
557        });
558        sum.variant_for(variant_name).unwrap_or_else(|| {
559            panic!(
560                "MsgpackTagged: variant {variant_name:?} of enum {name:?} not found in \
561                 registered Sum — `#[derive(MsgpackTagged)]` and `serde::Serialize` disagree \
562                 on variant names"
563            )
564        })
565    }
566
567    /// Write the outer `{variant_tag: <payload>}` map header common to all
568    /// four variant shapes — looks up the variant, writes a 1-entry msgpack
569    /// map header, and writes the variant tag as the map key. Returns the
570    /// resolved variant so callers can use its `payload` for the rest of the
571    /// shape (a payload map for tuple/struct, the inner value for newtype,
572    /// `nil` for unit).
573    fn write_variant_header(
574        &mut self,
575        name: &'static str,
576        variant_name: &'static str,
577    ) -> Result<crate::Variant, RmpError> {
578        let v = self.variant_for(name, variant_name);
579        write_map_header(self.inner.get_mut(), 1)?;
580        ser::Serializer::serialize_u8(&mut *self, v.tag)?;
581        Ok(v)
582    }
583}
584
585/// Build the inner `rmp_serde::Serializer` for primitive / forwarded
586/// calls. Single construction point so every spawn site
587/// ([`Serializer::new`], [`Serializer::sub_serializer_into`]) stays in
588/// lockstep.
589///
590/// **Why we don't apply `BytesMode::ForceIterables` here** (despite
591/// the legacy `acir::serialization::msgpack_serialize` doing so):
592///
593/// `ForceIterables` makes `rmp_serde::Serializer::collect_seq` detect
594/// byte-shaped iterators and emit msgpack `bin` instead of a
595/// `fixarray` of `fixint`s — the wire shape the C++ codegen's
596/// `std::vector<uint8_t>` adapter expects. But for that detection to
597/// apply, the call has to *reach* the inner's `collect_seq`. Our
598/// wrapper intercepts `Vec<T>::serialize` via the default
599/// `collect_seq` → `Self::serialize_seq` path: `serialize_seq` writes
600/// a `fixarray` header directly and routes each element through this
601/// wrapper, so the inner's `ForceIterables` is never consulted.
602///
603/// We could override `collect_seq` to forward byte-shaped iterators
604/// to `inner.collect_seq` — but rmp_serde's detection heuristic is
605/// purely size-based: any iterator over pointer-sized items (`&u8`,
606/// `Box<T>`, `Rc<T>`, `&T`, …) matches. For items that *aren't*
607/// actually `u8`, rmp_serde's `OnlyBytes` probe rejects, and rmp_serde
608/// falls back to its **own** `serialize_seq` — which doesn't route
609/// through our wrapper. That would silently bypass `MsgpackTagged`
610/// interception for any tagged type wrapped in `Box`/`&`/etc. inside
611/// a `Vec`. Hard to debug, easy to land in.
612///
613/// Instead we keep the wrapper simple and require the load-bearing
614/// case (`FieldElement`'s `Serialize` impl) to call
615/// `serializer.serialize_bytes(...)` directly — bypassing
616/// `collect_seq` entirely. `serialize_bytes` is `rmp_serde`'s
617/// unconditional `write_bin` (independent of `BytesMode`), and our
618/// own `serialize_bytes` forwards it untouched, so the wire is
619/// reliably `bin` for that field.
620///
621/// **If a future model adds another byte-shaped value type**, prefer
622/// hooking it up via `serialize_bytes` for the same reason. Only if a
623/// generic byte-iter intercept becomes truly necessary should we
624/// override `collect_seq` here — and at that point we'd need to
625/// **also replicate rmp_serde's `OnlyBytes` probe** so the
626/// reference-bypass risk above is closed.
627fn make_inner_rmp_serializer<W: Write>(writer: W) -> RmpSerializer<W> {
628    RmpSerializer::new(writer)
629}
630
631/// Write a msgpack array header (`fixarray` / `array16` / `array32` depending
632/// on `len`) directly to the underlying writer. Used by sequences and tuples.
633fn write_array_header<W: Write>(writer: &mut W, len: usize) -> Result<(), RmpError> {
634    let len_u32: u32 =
635        len.try_into().map_err(|_| RmpError::custom("array length doesn't fit in u32"))?;
636    rmp::encode::write_array_len(writer, len_u32)
637        .map_err(|e| RmpError::custom(format!("failed to write msgpack array header: {e}")))?;
638    Ok(())
639}
640
641/// Auto-downgrade `Array` → `Tagged` for products where the positional
642/// shape can't safely round-trip the type's own writes.
643///
644/// **The hazard.** Under `Array` the encoder only writes active fields
645/// (in tag-ascending order); the decoder walks a merged-sorted layout of
646/// `(active + reserved)` tags so it can drain reserved slots from
647/// *legacy* wires (V1 wrote a value at a tag that V2 has since retired).
648/// That works fine when reserved tags are all *strictly greater* than
649/// every active tag: the merged layout puts them at the tail, and the
650/// decoder hits `wire_remaining == 0` before visiting them. But if any
651/// reserved tag has an active tag *after* it in tag order, the merged
652/// layout interleaves a reserved slot in the middle. A round-trip of the
653/// type's own write would then drain a wire byte the encoder intended
654/// for the next active field, corrupting the decode.
655///
656/// **The fix.** When that interleaving is possible, the strategy
657/// silently flips to `Tagged` for this product — the int-keyed-map shape
658/// is self-describing on the wire (each entry carries its own tag) so
659/// the decoder doesn't need positional alignment. The wire is slightly
660/// larger but the type is now round-trip-safe.
661///
662/// **Why a silent downgrade rather than an error.** A bulk
663/// `with_default_strategy(Array)` is the common config — flipping it to
664/// an error per type would force callers to add a `with_strategy::<T>(Tagged)`
665/// override for every schema-evolved leaf, which is noise. The downgrade
666/// is local to the call and doesn't affect other types in the same
667/// serializer. Documented in the crate README under the migration guide.
668fn downgrade_array_if_unsafe(
669    product: &crate::Product,
670    strategy: EncodingStrategy,
671) -> EncodingStrategy {
672    if strategy != EncodingStrategy::Array || product.reserved.is_empty() {
673        return strategy;
674    }
675    let max_active = product.fields.iter().map(|(t, _)| *t).max();
676    let min_reserved = product.reserved.iter().copied().min();
677    match (max_active, min_reserved) {
678        // Some reserved tag falls at or before some active tag — the
679        // unsafe interleaving case. Downgrade.
680        (Some(active), Some(reserved)) if reserved <= active => EncodingStrategy::Tagged,
681        // Either no active fields (the product is empty so nothing
682        // round-trips through Array anyway) or reserved is strictly
683        // trailing — Array is safe.
684        _ => strategy,
685    }
686}
687
688/// Write the outer Product header in the shape required by `strategy`:
689/// `Tagged` → int-keyed `fixmap`, `Array` → positional `fixarray`. Used
690/// by both top-level struct paths and variant-payload paths.
691fn write_strategy_header<W: Write>(
692    writer: &mut W,
693    len: usize,
694    strategy: EncodingStrategy,
695) -> Result<(), RmpError> {
696    match strategy {
697        EncodingStrategy::Tagged => write_map_header(writer, len),
698        EncodingStrategy::Array => write_array_header(writer, len),
699    }
700}
701
702/// Assert that serde's reported field count matches the registered
703/// `Product`'s — both should drop the same skipped fields, so they should
704/// always agree. A mismatch is a real misconfiguration: the most common
705/// cause is a `PhantomData<T>` field that's missing `#[serde(skip)]`,
706/// which would otherwise fail later with a confusing "field not found in
707/// registered Product" error from a tag lookup. The assert surfaces it
708/// earlier, with a more actionable message.
709fn assert_field_count_matches(name: &str, serde_len: usize, product_len: usize) {
710    assert_eq!(
711        serde_len, product_len,
712        "MsgpackTagged: serde reports {serde_len} fields for {name:?} but the registered \
713         Product carries {product_len}. The macro and `serde::Serialize` disagree on \
714         which fields are on the wire — typically because a `PhantomData<T>` field \
715         is missing `#[serde(skip)]`",
716    );
717}
718
719/// Write a msgpack map header (`fixmap` / `map16` / `map32` depending on
720/// `len`) directly to the underlying writer. Used by structs, maps, and the
721/// variant shapes once those land.
722fn write_map_header<W: Write>(writer: &mut W, len: usize) -> Result<(), RmpError> {
723    let len_u32: u32 =
724        len.try_into().map_err(|_| RmpError::custom("map length doesn't fit in u32"))?;
725    rmp::encode::write_map_len(writer, len_u32)
726        .map_err(|e| RmpError::custom(format!("failed to write msgpack map header: {e}")))?;
727    Ok(())
728}
729
730/// Adapter for product shapes — both named structs and multi-element tuple
731/// structs go through here. The two trait impls below differ only in how
732/// they resolve a serde call to a wire tag: named-struct calls carry a
733/// field-name string, tuple-struct calls carry an implicit position counter.
734/// The map header is already written in the corresponding `serialize_*`
735/// method before this adapter is constructed; from there each
736/// `serialize_field` call appends a `(tag, value)` pair to the writer
737/// through the parent [`Serializer`], so any nested tagged
738/// value in `value` recurses through the wrapper instead of falling through
739/// to `rmp_serde`'s default positional-array struct encoding.
740///
741/// `next_position` is only consulted by the [`SerializeTupleStruct`] impl;
742/// the [`SerializeStruct`] impl ignores it.
743///
744/// `strategy` drives the per-field emission: under `Tagged` each entry is
745/// `(tag, value)` on the wire, under `Array` each entry is just `value`.
746///
747/// Behavior splits on the `buffer` flag, decided at `begin_product` time:
748///
749/// * `buffer = false` (the common path): the outer header is written
750///   upfront and each `serialize_field` writes through the parent stream
751///   immediately. Used when reordering can't change the wire (`Tagged`
752///   with any source order, or `Array` whose source order is already
753///   tag-ascending). The `entries` field stays empty.
754/// * `buffer = true`: the outer header is *deferred* and `serialize_field`
755///   encodes the value into a fresh `Vec<u8>` through a sub-serializer
756///   that shares the parent's registry + strategy, then pushes
757///   `(tag, bytes)` to `entries`. The `finish` flush at `end()` sorts by
758///   tag and dumps in canonical order to the parent. Only reached for
759///   `Array`-strategy types whose source-declaration order doesn't match
760///   tag-ascending order.
761pub struct TaggedSerializeProduct<'ser, 'a, W: Write> {
762    product: crate::Product,
763    parent: &'ser mut Serializer<'a, W>,
764    next_position: usize,
765    strategy: EncodingStrategy,
766    buffer: bool,
767    entries: Vec<(u8, Vec<u8>)>,
768}
769
770/// Decide whether to buffer + flush in tag-ascending order, write the
771/// outer header upfront in the direct case, and return the configured
772/// adapter. Shared by [`Serializer::begin_product`] (top-level) and
773/// [`Serializer::begin_variant_payload`] (enum payload).
774fn begin_product_payload<'ser, 'a, W: Write>(
775    parent: &'ser mut Serializer<'a, W>,
776    product: crate::Product,
777    strategy: EncodingStrategy,
778) -> Result<TaggedSerializeProduct<'ser, 'a, W>, RmpError> {
779    // Buffer iff serde's call order won't naturally produce
780    // tag-ascending output — i.e. whenever the user's source-declaration
781    // order has been deliberately reordered relative to the tags. Both
782    // strategies benefit from canonical wire order (Array for correctness,
783    // Tagged for byte-determinism — cross-implementation compat, hashing
784    // / commitment use cases). Types whose source order *is* monotonic
785    // (the vast majority) pay no allocation regardless of strategy.
786    let buffer = !product.tag_order_matches_source;
787    if !buffer {
788        write_strategy_header(parent.inner.get_mut(), product.fields.len(), strategy)?;
789    }
790    Ok(TaggedSerializeProduct {
791        // Capacity matters only on the buffered path; on the direct path
792        // entries stays empty and the heap allocation is skipped.
793        entries: if buffer { Vec::with_capacity(product.fields.len()) } else { Vec::new() },
794        product,
795        parent,
796        next_position: 0,
797        strategy,
798        buffer,
799    })
800}
801
802impl<'ser, 'a, W: Write> TaggedSerializeProduct<'ser, 'a, W> {
803    /// Emit one field's wire contribution. Direct path (the common case):
804    /// write `(tag, value)` under Tagged or just `value` under Array,
805    /// straight to the parent stream. Buffered path (Array with
806    /// non-monotonic source order): encode the value into a temp
807    /// `Vec<u8>` through a sub-serializer that shares the parent's
808    /// registry + strategy and push `(tag, bytes)` to `entries` for
809    /// later flushing in `finish`. Either path keeps nested tagged
810    /// types recursing through the wrapper.
811    fn serialize_tag_and_value<T>(&mut self, tag: u8, value: &T) -> Result<(), RmpError>
812    where
813        T: ?Sized + Serialize,
814    {
815        if self.buffer {
816            let mut buf = Vec::new();
817            {
818                let mut sub = self.parent.sub_serializer_into(&mut buf);
819                value.serialize(&mut sub)?;
820            }
821            self.entries.push((tag, buf));
822        } else {
823            if matches!(self.strategy, EncodingStrategy::Tagged) {
824                ser::Serializer::serialize_u8(&mut *self.parent, tag)?;
825            }
826            value.serialize(&mut *self.parent)?;
827        }
828        Ok(())
829    }
830
831    /// Flush the deferred state: under the direct path the header and
832    /// every value have already been written, so this is a no-op. Under
833    /// the buffered path, sort entries by tag, write the strategy-
834    /// appropriate header, then emit each `(tag-prefix?, value-bytes)`
835    /// pair to the parent.
836    fn finish(mut self) -> Result<(), RmpError> {
837        if !self.buffer {
838            return Ok(());
839        }
840        // Stable sort because tags are unique within a Product (macro
841        // guarantees no duplicate tags), so any stability quirk would be
842        // a registry bug, not a serialization bug.
843        self.entries.sort_by_key(|(tag, _)| *tag);
844        write_strategy_header(self.parent.inner.get_mut(), self.entries.len(), self.strategy)?;
845        for (tag, bytes) in &self.entries {
846            if matches!(self.strategy, EncodingStrategy::Tagged) {
847                ser::Serializer::serialize_u8(&mut *self.parent, *tag)?;
848            }
849            self.parent.inner.get_mut().write_all(bytes).map_err(|e| {
850                RmpError::custom(format!("failed to flush buffered field bytes: {e}"))
851            })?;
852        }
853        Ok(())
854    }
855}
856
857/// Named-field struct (`struct Foo { a: u32, b: bool }`). Each
858/// `serialize_field(name, value)` call resolves `name` against the
859/// registered `Product` (honoring `#[serde(rename)]`) to derive the wire
860/// tag, then writes `tag` and `value` through the parent.
861impl<'ser, 'a, W: Write> SerializeStruct for TaggedSerializeProduct<'ser, 'a, W> {
862    type Ok = ();
863    type Error = RmpError;
864
865    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
866    where
867        T: ?Sized + Serialize,
868    {
869        let tag = self.product.tag_for(key).ok_or_else(|| {
870            RmpError::custom(format!(
871                "MsgpackTagged: field {key:?} not found in registered Product — \
872                 this struct's `#[derive(MsgpackTagged)]` and `serde::Serialize` \
873                 disagree on field names (check `#[serde(rename = ...)]`)",
874            ))
875        })?;
876        self.serialize_tag_and_value(tag, value)
877    }
878
879    fn end(self) -> Result<Self::Ok, Self::Error> {
880        self.finish()
881    }
882}
883
884/// Multi-element tuple struct (`struct Pair(u32, bool)`). serde calls
885/// `serialize_field(value)` once per element in source-declaration order
886/// without supplying a name, so we keep an internal position counter and
887/// look up the wire tag for the source position (the registered `Product`
888/// uses positional names `"0"`, `"1"`, … as wire-name strings). Resolving by
889/// position lets `#[tag(N)]`-reordered tuple structs (e.g.
890/// `struct Triple(#[tag(2)] u32, #[tag(0)] bool, #[tag(1)] u8)`) emit each
891/// field under the right wire tag even though the calls arrive in source
892/// order — and the buffer-and-flush in `finish` then writes them on the
893/// wire in tag-ascending order.
894impl<'ser, 'a, W: Write> SerializeTupleStruct for TaggedSerializeProduct<'ser, 'a, W> {
895    type Ok = ();
896    type Error = RmpError;
897
898    fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
899    where
900        T: ?Sized + Serialize,
901    {
902        let position = self.next_position;
903        self.next_position += 1;
904        // Wire-name strings are positional ("0", "1", …) — produced by the
905        // macro from `position.to_string()` lifted into a `&'static str`
906        // const. We allocate a fresh `String` per call to look it up; for
907        // the small (typically 2–5) field counts of tuple structs this is
908        // not in any hot path.
909        let position_name = position.to_string();
910        let tag = self.product.tag_for(&position_name).ok_or_else(|| {
911            RmpError::custom(format!(
912                "MsgpackTagged: tuple-struct position {position} not found in registered \
913                 Product — the macro's emitted `Product` has fewer fields than serde is \
914                 trying to serialize"
915            ))
916        })?;
917        self.serialize_tag_and_value(tag, value)
918    }
919
920    fn end(self) -> Result<Self::Ok, Self::Error> {
921        self.finish()
922    }
923}
924
925/// Tuple variant payload (`enum E { ... Pair(u32, bool) }`). Same payload
926/// shape and tag-resolution rule as a top-level tuple struct — the variant's
927/// `payload` `Product` uses positional names (`"0"`, `"1"`, …). The outer
928/// `{variant_tag: ...}` map (1-entry discriminator) was written upfront in
929/// `serialize_tuple_variant`; the payload's header is deferred to
930/// `finish()` along with every other product's.
931impl<'ser, 'a, W: Write> SerializeTupleVariant for TaggedSerializeProduct<'ser, 'a, W> {
932    type Ok = ();
933    type Error = RmpError;
934
935    fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
936    where
937        T: ?Sized + Serialize,
938    {
939        let position = self.next_position;
940        self.next_position += 1;
941        let position_name = position.to_string();
942        let tag = self.product.tag_for(&position_name).ok_or_else(|| {
943            RmpError::custom(format!(
944                "MsgpackTagged: tuple-variant position {position} not found in registered \
945                 Variant payload"
946            ))
947        })?;
948        self.serialize_tag_and_value(tag, value)
949    }
950
951    fn end(self) -> Result<Self::Ok, Self::Error> {
952        self.finish()
953    }
954}
955
956/// Struct variant payload (`enum E { ... Named { a: u32, b: bool } }`). Same
957/// payload shape and tag-resolution rule as a top-level named struct.
958impl<'ser, 'a, W: Write> SerializeStructVariant for TaggedSerializeProduct<'ser, 'a, W> {
959    type Ok = ();
960    type Error = RmpError;
961
962    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
963    where
964        T: ?Sized + Serialize,
965    {
966        let tag = self.product.tag_for(key).ok_or_else(|| {
967            RmpError::custom(format!(
968                "MsgpackTagged: field {key:?} not found in registered Variant payload — \
969                 `#[derive(MsgpackTagged)]` and `serde::Serialize` disagree on variant \
970                 field names (check `#[serde(rename = ...)]`)"
971            ))
972        })?;
973        self.serialize_tag_and_value(tag, value)
974    }
975
976    fn end(self) -> Result<Self::Ok, Self::Error> {
977        self.finish()
978    }
979}
980
981/// Stateless pass-through adapter shared by every shape whose only job is
982/// to route element/key/value calls back through the parent
983/// [`Serializer`]. The msgpack header (array length or map
984/// length) is written upfront in the corresponding `serialize_*` method
985/// before the adapter is constructed; from there each entry is just one or
986/// two more values appended to the writer through the wrapper, so any
987/// tagged value nested inside still gets the int-keyed-map treatment.
988///
989/// Used as `SerializeSeq` (e.g. `Vec<T>`), `SerializeTuple` (fixed-length
990/// Rust tuples), and `SerializeMap` (e.g. `BTreeMap<K, V>`). Struct shapes
991/// have their own adapter ([`TaggedSerializeProduct`]) because they carry
992/// the [`Product`](crate::Product) needed to translate field names into
993/// integer tags.
994pub struct TaggedSerializeViaParent<'ser, 'a, W: Write> {
995    parent: &'ser mut Serializer<'a, W>,
996}
997
998/// Variable-length sequences (`Vec<T>`, `&[T]`, …). Each element recurses
999/// through the parent so tagged elements stay int-keyed.
1000impl<'ser, 'a, W: Write> SerializeSeq for TaggedSerializeViaParent<'ser, 'a, W> {
1001    type Ok = ();
1002    type Error = RmpError;
1003
1004    fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
1005    where
1006        T: ?Sized + Serialize,
1007    {
1008        value.serialize(&mut *self.parent)
1009    }
1010
1011    fn end(self) -> Result<Self::Ok, Self::Error> {
1012        // msgpack arrays/maps are length-prefixed, not terminated — nothing
1013        // to write here.
1014        Ok(())
1015    }
1016}
1017
1018/// Fixed-length Rust tuples (`(A, B)`, `(A, B, C)`, …). Same wire shape as a
1019/// sequence — msgpack has one length-prefixed array, regardless of whether
1020/// the source was variable- or fixed-length on the Rust side.
1021impl<'ser, 'a, W: Write> SerializeTuple for TaggedSerializeViaParent<'ser, 'a, W> {
1022    type Ok = ();
1023    type Error = RmpError;
1024
1025    fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
1026    where
1027        T: ?Sized + Serialize,
1028    {
1029        value.serialize(&mut *self.parent)
1030    }
1031
1032    fn end(self) -> Result<Self::Ok, Self::Error> {
1033        Ok(())
1034    }
1035}
1036
1037/// Free-form maps (`BTreeMap<K, V>` and friends). Both keys and values are
1038/// routed through the parent. Routing keys is mostly a no-op for the common
1039/// primitive-key case (the wrapper forwards primitives to inner verbatim),
1040/// but it keeps the door open for tagged keys without a special case here.
1041impl<'ser, 'a, W: Write> SerializeMap for TaggedSerializeViaParent<'ser, 'a, W> {
1042    type Ok = ();
1043    type Error = RmpError;
1044
1045    fn serialize_key<T>(&mut self, key: &T) -> Result<(), Self::Error>
1046    where
1047        T: ?Sized + Serialize,
1048    {
1049        key.serialize(&mut *self.parent)
1050    }
1051
1052    fn serialize_value<T>(&mut self, value: &T) -> Result<(), Self::Error>
1053    where
1054        T: ?Sized + Serialize,
1055    {
1056        value.serialize(&mut *self.parent)
1057    }
1058
1059    fn end(self) -> Result<Self::Ok, Self::Error> {
1060        Ok(())
1061    }
1062}
1063
1064#[cfg(test)]
1065mod builder_tests {
1066    //! Tests for `Serializer::new` + `with_default_strategy` +
1067    //! `with_strategy`. Phase 1 — the strategy state is configured but the
1068    //! encoder doesn't yet branch on it; these tests verify the *state* is
1069    //! recorded correctly via `Serializer::strategy_for`. Phase 2 will add
1070    //! encode-side dispatch and tests for the actual wire shape.
1071
1072    use super::*;
1073    use crate::Tagged;
1074
1075    struct Foo;
1076    impl MsgpackTagged for Foo {
1077        const TAGGED: Tagged = Tagged::empty_product();
1078        fn register_into(reg: &mut TagRegistry) {
1079            reg.try_insert::<Self>("Foo");
1080        }
1081    }
1082
1083    struct Bar;
1084    impl MsgpackTagged for Bar {
1085        const TAGGED: Tagged = Tagged::empty_product();
1086        fn register_into(reg: &mut TagRegistry) {
1087            reg.try_insert::<Self>("Bar");
1088        }
1089    }
1090
1091    /// Build a registry that contains every type passed in. Test-local
1092    /// helper — production registries are built via
1093    /// `TagRegistry::from_type::<T>()` which seeds the walk from one
1094    /// top-level type.
1095    fn registry_of<F: FnOnce(&mut TagRegistry)>(f: F) -> TagRegistry {
1096        let mut reg = TagRegistry::new();
1097        f(&mut reg);
1098        reg
1099    }
1100
1101    #[test]
1102    fn default_strategy_is_tagged() {
1103        let registry = TagRegistry::from_type::<Foo>();
1104        let s = Serializer::new(Vec::<u8>::new(), &registry);
1105        assert_eq!(s.strategy_for::<Foo>(), EncodingStrategy::Tagged);
1106    }
1107
1108    #[test]
1109    fn with_default_strategy_changes_default() {
1110        let registry = TagRegistry::from_type::<Foo>();
1111        let s = Serializer::new(Vec::<u8>::new(), &registry)
1112            .with_default_strategy(EncodingStrategy::Array);
1113        assert_eq!(s.strategy_for::<Foo>(), EncodingStrategy::Array);
1114    }
1115
1116    #[test]
1117    fn per_type_override_beats_default() {
1118        let registry = TagRegistry::from_type::<Foo>();
1119        let s = Serializer::new(Vec::<u8>::new(), &registry)
1120            .with_default_strategy(EncodingStrategy::Array)
1121            .with_strategy::<Foo>(EncodingStrategy::Tagged);
1122        assert_eq!(s.strategy_for::<Foo>(), EncodingStrategy::Tagged);
1123    }
1124
1125    #[test]
1126    fn per_type_overrides_are_independent() {
1127        let registry = registry_of(|r| {
1128            Foo::register_into(r);
1129            Bar::register_into(r);
1130        });
1131        let s = Serializer::new(Vec::<u8>::new(), &registry)
1132            .with_default_strategy(EncodingStrategy::Array)
1133            .with_strategy::<Foo>(EncodingStrategy::Tagged);
1134        // Bar has no override; falls back to the (changed) default.
1135        assert_eq!(s.strategy_for::<Foo>(), EncodingStrategy::Tagged);
1136        assert_eq!(s.strategy_for::<Bar>(), EncodingStrategy::Array);
1137    }
1138
1139    #[test]
1140    fn last_with_strategy_wins() {
1141        let registry = TagRegistry::from_type::<Foo>();
1142        let s = Serializer::new(Vec::<u8>::new(), &registry)
1143            .with_strategy::<Foo>(EncodingStrategy::Array)
1144            .with_strategy::<Foo>(EncodingStrategy::Tagged);
1145        assert_eq!(s.strategy_for::<Foo>(), EncodingStrategy::Tagged);
1146    }
1147
1148    /// Setting a strategy for a type the registry never saw is almost
1149    /// always a type-graph miss bug — the override would silently never
1150    /// fire. `with_strategy` panics so the misuse surfaces at config
1151    /// time.
1152    #[test]
1153    #[should_panic(expected = "is not in the registry")]
1154    fn with_strategy_panics_on_unregistered_type() {
1155        let registry = TagRegistry::from_type::<Foo>();
1156        let _ = Serializer::new(Vec::<u8>::new(), &registry)
1157            .with_strategy::<Bar>(EncodingStrategy::Array);
1158    }
1159
1160    /// `with_strategy_for_name` is the policy-site sibling — never
1161    /// asserts. Inserting an override for a name the registry doesn't
1162    /// know about is a no-op at encode time (the name never matches any
1163    /// `serialize_struct` call), so `strategy_for::<Foo>` still resolves
1164    /// the configured default.
1165    #[test]
1166    fn with_strategy_for_name_does_not_assert_registration() {
1167        let registry = TagRegistry::from_type::<Foo>();
1168        let s = Serializer::new(Vec::<u8>::new(), &registry)
1169            .with_default_strategy(EncodingStrategy::Array)
1170            .with_strategy_for_name("Bar", EncodingStrategy::Tagged);
1171        assert_eq!(s.strategy_for::<Foo>(), EncodingStrategy::Array);
1172    }
1173}