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, ®istry)
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, ®istry);
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(), ®istry);
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(), ®istry)
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(), ®istry)
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(), ®istry)
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(), ®istry)
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(), ®istry)
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(), ®istry)
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}