Skip to main content

buffa_types/
any_ext.rs

1//! Ergonomic helpers for [`google::protobuf::Any`](crate::google::protobuf::Any).
2
3use alloc::string::String;
4
5use crate::google::protobuf::Any;
6
7impl Any {
8    /// Pack a message into an [`Any`] with the given type URL.
9    ///
10    /// The type URL is conventionally of the form
11    /// `type.googleapis.com/fully.qualified.TypeName`, but this method does
12    /// not enforce that convention — any string is accepted.
13    ///
14    /// # Panics
15    ///
16    /// Panics if `msg`'s encoded size exceeds the 2 GiB protobuf limit
17    /// ([`buffa::MAX_MESSAGE_BYTES`]) — see [`try_pack`](Self::try_pack)
18    /// for the error-returning variant.
19    pub fn pack(msg: &impl buffa::Message, type_url: impl Into<String>) -> Self {
20        Self {
21            type_url: type_url.into(),
22            value: msg.encode_to_bytes(),
23            ..Default::default()
24        }
25    }
26
27    /// Pack a message into an [`Any`], returning an error instead of
28    /// panicking if the message's encoded size exceeds the 2 GiB protobuf
29    /// limit ([`buffa::MAX_MESSAGE_BYTES`]).
30    ///
31    /// # Errors
32    ///
33    /// Returns [`buffa::EncodeError::MessageTooLarge`] if the encoded size
34    /// exceeds the limit.
35    pub fn try_pack(
36        msg: &impl buffa::Message,
37        type_url: impl Into<String>,
38    ) -> Result<Self, buffa::EncodeError> {
39        Ok(Self {
40            type_url: type_url.into(),
41            value: msg.try_encode_to_bytes()?,
42            ..Default::default()
43        })
44    }
45
46    /// Unpack the contained message, decoding its bytes as `T`, **without
47    /// checking the `type_url`**.
48    ///
49    /// This method always attempts to decode the payload as `T` regardless
50    /// of whether `type_url` actually identifies `T`. Use [`Any::unpack_if`]
51    /// when you need to verify the stored type before decoding.
52    ///
53    /// # Errors
54    ///
55    /// Returns a [`buffa::DecodeError`] if the bytes cannot be decoded as `T`.
56    pub fn unpack_unchecked<T: buffa::Message>(&self) -> Result<T, buffa::DecodeError> {
57        T::decode(&mut self.value.as_ref())
58    }
59
60    /// Unpack the contained message as `T`, but only if the `type_url`
61    /// matches `expected_type_url`.
62    ///
63    /// Returns `Ok(None)` when the type URL does not match.
64    ///
65    /// # Errors
66    ///
67    /// Returns a [`buffa::DecodeError`] if the type URL matches but the bytes
68    /// cannot be decoded as `T`.
69    pub fn unpack_if<T: buffa::Message>(
70        &self,
71        expected_type_url: &str,
72    ) -> Result<Option<T>, buffa::DecodeError> {
73        if self.type_url != expected_type_url {
74            return Ok(None);
75        }
76        T::decode(&mut self.value.as_ref()).map(Some)
77    }
78
79    /// Returns `true` if this [`Any`]'s `type_url` matches the given string.
80    pub fn is_type(&self, type_url: &str) -> bool {
81        self.type_url == type_url
82    }
83
84    /// Returns the type URL stored in this [`Any`].
85    pub fn type_url(&self) -> &str {
86        &self.type_url
87    }
88}
89
90// ── WKT type registry ───────────────────────────────────────────────────────
91
92/// Registers all well-known types with the given [`TypeRegistry`].
93///
94/// This registers Duration, Timestamp, FieldMask, Value, Struct, ListValue,
95/// Empty, all wrapper types, and Any itself, enabling both proto3-compliant
96/// JSON serialization (under the `json` feature) and textproto
97/// `[type_url] { fields }` Any-expansion when these types appear inside
98/// `google.protobuf.Any` fields.
99///
100/// Text entries are always registered (buffa-types unconditionally enables
101/// `buffa/text`). JSON entries are registered under the `json` feature.
102///
103/// # Example
104///
105/// ```rust,no_run
106/// use buffa::type_registry::{TypeRegistry, set_type_registry};
107///
108/// let mut reg = TypeRegistry::new();
109/// buffa_types::register_wkt_types(&mut reg);
110/// set_type_registry(reg);
111/// ```
112///
113/// [`TypeRegistry`]: buffa::type_registry::TypeRegistry
114pub fn register_wkt_types(reg: &mut buffa::type_registry::TypeRegistry) {
115    use crate::google::protobuf::*;
116    use buffa::type_registry::{any_encode_text, any_merge_text, TextAnyEntry};
117
118    macro_rules! register_type {
119        ($type:ty, $wkt:expr) => {
120            #[cfg(feature = "json")]
121            {
122                use alloc::string::ToString;
123                reg.register_json_any(buffa::type_registry::JsonAnyEntry {
124                    type_url: <$type>::TYPE_URL,
125                    to_json: |bytes| {
126                        let msg = <$type as buffa::Message>::decode(&mut &*bytes)
127                            .map_err(|e| e.to_string())?;
128                        serde_json::to_value(&msg).map_err(|e| e.to_string())
129                    },
130                    from_json: |value| {
131                        let msg: $type =
132                            serde_json::from_value(value).map_err(|e| e.to_string())?;
133                        buffa::Message::try_encode_to_vec(&msg).map_err(|e| e.to_string())
134                    },
135                    is_wkt: $wkt,
136                });
137            }
138            // WKTs all implement TextFormat (generate_text is on for
139            // buffa-types). Non-Option fn-ptrs — presence in the text map
140            // means text-capable. `$wkt` is irrelevant here: textproto has
141            // no `"value"` wrapping distinction.
142            reg.register_text_any(TextAnyEntry {
143                type_url: <$type>::TYPE_URL,
144                text_encode: any_encode_text::<$type>,
145                text_merge: any_merge_text::<$type>,
146            });
147        };
148    }
149
150    // WKTs with special JSON mappings (use "value" wrapping in Any JSON).
151    register_type!(Duration, true);
152    register_type!(Timestamp, true);
153    register_type!(FieldMask, true);
154    register_type!(Value, true);
155    register_type!(Struct, true);
156    register_type!(ListValue, true);
157    register_type!(BoolValue, true);
158    register_type!(Int32Value, true);
159    register_type!(UInt32Value, true);
160    register_type!(Int64Value, true);
161    register_type!(UInt64Value, true);
162    register_type!(FloatValue, true);
163    register_type!(DoubleValue, true);
164    register_type!(StringValue, true);
165    register_type!(BytesValue, true);
166    register_type!(Any, true);
167
168    // Regular messages (fields inlined in Any JSON).
169    register_type!(Empty, false);
170}
171
172// ── TextFormat impl ─────────────────────────────────────────────────────────
173//
174// Hand-written because textproto packs `Any` as `[type_url] { fields }` when
175// the type is registered — a shape the generated field-by-field impl can't
176// produce. Codegen's `impl_text.rs` skips `google.protobuf.Any` to avoid a
177// conflicting impl.
178//
179// `try_write_any_expanded` and `read_any_expansion` consult the text-format
180// Any map (installed via `set_type_registry`). When no registry is installed,
181// this degrades to the vanilla `type_url: "..." value: "..."` form — still
182// valid textproto, just not the expanded form.
183
184impl buffa::text::TextFormat for Any {
185    fn encode_text(&self, enc: &mut buffa::text::TextEncoder<'_>) -> core::fmt::Result {
186        if !self.type_url.is_empty() && enc.try_write_any_expanded(&self.type_url, &self.value)? {
187            return Ok(());
188        }
189        // Vanilla fallback: unregistered type, or no registry installed.
190        if !self.type_url.is_empty() {
191            enc.write_field_name("type_url")?;
192            enc.write_string(&self.type_url)?;
193        }
194        if !self.value.is_empty() {
195            enc.write_field_name("value")?;
196            enc.write_bytes(&self.value)?;
197        }
198        Ok(())
199    }
200
201    fn merge_text(
202        &mut self,
203        dec: &mut buffa::text::TextDecoder<'_>,
204    ) -> Result<(), buffa::text::ParseError> {
205        while let Some(name) = dec.read_field_name()? {
206            match name {
207                "type_url" => self.type_url = dec.read_string()?.into_owned(),
208                "value" => self.value = dec.read_bytes()?.into(),
209                _ if name.starts_with('[') => {
210                    let (url, bytes) = dec.read_any_expansion(name)?;
211                    self.type_url = url.into();
212                    self.value = bytes.into();
213                }
214                _ => dec.skip_value()?,
215            }
216        }
217        Ok(())
218    }
219}
220
221#[cfg(test)]
222mod text_tests {
223    use super::Any;
224    use buffa::text::{decode_from_str, encode_to_string};
225
226    #[test]
227    fn vanilla_roundtrip_no_registry() {
228        // Without a registry installed, Any uses the plain
229        // `type_url: "..." value: "..."` form — exactly what the old
230        // generated impl did.
231        let orig = Any {
232            type_url: "type.example.com/Foo".into(),
233            value: alloc::vec![0x08, 0x2A].into(), // field 1 = varint 42
234            ..Default::default()
235        };
236        let text = encode_to_string(&orig);
237        assert_eq!(text, r#"type_url: "type.example.com/Foo" value: "\010*""#);
238        let back: Any = decode_from_str(&text).unwrap();
239        assert_eq!(back.type_url, orig.type_url);
240        assert_eq!(back.value, orig.value);
241    }
242
243    // Registry-manipulating tests live in `serde_tests` below — they share
244    // the same global `AtomicPtr` as the JSON tests and must use the same
245    // `REGISTRY_LOCK` to serialize.
246}
247
248// ── serde impls ──────────────────────────────────────────────────────────────
249//
250// Proto3 JSON for `Any` uses the global `AnyRegistry` to serialize the
251// embedded message with its fields inline (regular messages) or wrapped in a
252// `"value"` key (WKTs). Falls back to base64-encoded `value` when the
253// registry is absent or the type URL is not registered.
254
255#[cfg(feature = "json")]
256struct Base64Bytes<'a>(&'a [u8]);
257
258#[cfg(feature = "json")]
259impl serde::Serialize for Base64Bytes<'_> {
260    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
261        buffa::json_helpers::bytes::serialize(self.0, s)
262    }
263}
264
265#[cfg(feature = "json")]
266impl serde::Serialize for Any {
267    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
268        use serde::ser::SerializeMap;
269
270        if self.type_url.is_empty() {
271            return s.serialize_map(Some(0))?.end();
272        }
273
274        let lookup = buffa::any_registry::with_any_registry(|reg| {
275            reg.and_then(|r| r.lookup(&self.type_url))
276                .map(|e| (e.to_json, e.is_wkt))
277        });
278
279        match lookup {
280            Some((to_json, is_wkt)) => {
281                let json_val = to_json(&self.value).map_err(serde::ser::Error::custom)?;
282                if is_wkt {
283                    let mut map = s.serialize_map(Some(2))?;
284                    map.serialize_entry("@type", &self.type_url)?;
285                    map.serialize_entry("value", &json_val)?;
286                    map.end()
287                } else {
288                    let fields = match &json_val {
289                        serde_json::Value::Object(m) => m,
290                        _ => {
291                            return Err(serde::ser::Error::custom(
292                                "Any: to_json for non-WKT must return a JSON object",
293                            ))
294                        }
295                    };
296                    let mut map = s.serialize_map(Some(1 + fields.len()))?;
297                    map.serialize_entry("@type", &self.type_url)?;
298                    for (k, v) in fields {
299                        map.serialize_entry(k, v)?;
300                    }
301                    map.end()
302                }
303            }
304            None => {
305                let mut map = s.serialize_map(Some(2))?;
306                map.serialize_entry("@type", &self.type_url)?;
307                map.serialize_entry("value", &Base64Bytes(&self.value))?;
308                map.end()
309            }
310        }
311    }
312}
313
314#[cfg(feature = "json")]
315impl<'de> serde::Deserialize<'de> for Any {
316    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
317        // Buffer the entire object so @type can appear at any position.
318        let mut obj: serde_json::Map<String, serde_json::Value> =
319            serde::Deserialize::deserialize(d)?;
320
321        let type_url = match obj.remove("@type") {
322            Some(serde_json::Value::String(s)) => s,
323            Some(_) => {
324                return Err(serde::de::Error::custom("@type must be a string"));
325            }
326            None => return Ok(Self::default()),
327        };
328
329        // The type URL must be non-empty and contain a '/' separating the
330        // host/authority from the fully-qualified type name (e.g.
331        // "type.googleapis.com/google.protobuf.Duration").
332        if type_url.is_empty() || !type_url.contains('/') {
333            return Err(serde::de::Error::custom(
334                "@type must be a valid type URL containing a '/' (e.g. type.googleapis.com/pkg.Type)",
335            ));
336        }
337
338        let lookup = buffa::any_registry::with_any_registry(|reg| {
339            reg.and_then(|r| r.lookup(&type_url))
340                .map(|e| (e.from_json, e.is_wkt))
341        });
342
343        let value = match lookup {
344            Some((from_json, true)) => {
345                let json_val = obj.remove("value").unwrap_or(serde_json::Value::Null);
346                from_json(json_val).map_err(serde::de::Error::custom)?
347            }
348            Some((from_json, false)) => {
349                let json_obj = serde_json::Value::Object(obj);
350                from_json(json_obj).map_err(serde::de::Error::custom)?
351            }
352            None => {
353                // Fallback: base64 decode the "value" field.
354                match obj.remove("value") {
355                    Some(serde_json::Value::String(s)) => buffa::json_helpers::bytes::deserialize(
356                        serde::de::value::StringDeserializer::<D::Error>::new(s),
357                    )?,
358                    _ => alloc::vec::Vec::new(),
359                }
360            }
361        };
362
363        Ok(Self {
364            type_url,
365            value: value.into(),
366            ..Default::default()
367        })
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::google::protobuf::Timestamp;
375    use buffa::Message as _;
376
377    #[test]
378    fn any_view_to_owned_from_source_is_zero_copy() {
379        use crate::google::protobuf::__buffa::view::AnyView;
380        use buffa::view::{MessageView as _, OwnedView};
381
382        let src = Any {
383            type_url: "type.googleapis.com/x".into(),
384            value: bytes::Bytes::from_static(&[1u8; 256]),
385            ..Default::default()
386        };
387        let buf = bytes::Bytes::from(src.encode_to_vec());
388
389        // Direct trait path: to_owned_from_source(Some(&buf)) → slice_ref.
390        let view = AnyView::decode_view(&buf).unwrap();
391        let owned = view.to_owned_from_source(Some(&buf)).unwrap();
392        assert_eq!(owned.value, src.value);
393        let value_ptr = owned.value.as_ptr() as usize;
394        let buf_range = (buf.as_ptr() as usize)..(buf.as_ptr() as usize + buf.len());
395        assert!(
396            buf_range.contains(&value_ptr),
397            "owned.value should point into buf (slice_ref), got {value_ptr:#x} outside {buf_range:#x?}"
398        );
399
400        // OwnedView path: the inherent OwnedView::to_owned_message routes
401        // through to_owned_from_source(Some(&self.bytes)), so the bytes field
402        // is a zero-copy slice_ref into the retained buffer.
403        let ov = OwnedView::<AnyView<'static>>::decode(buf.clone()).unwrap();
404        let owned2 = ov.to_owned_message();
405        assert_eq!(owned2.value, src.value);
406        assert!(buf_range.contains(&(owned2.value.as_ptr() as usize)));
407
408        // No-source path still copies (correct, distinct allocation).
409        let copied = view.to_owned_message().unwrap();
410        assert_eq!(copied.value, src.value);
411        assert!(!buf_range.contains(&(copied.value.as_ptr() as usize)));
412    }
413
414    #[cfg(feature = "arbitrary")]
415    #[test]
416    fn any_arbitrary_with_bytes_value() {
417        use arbitrary::{Arbitrary, Unstructured};
418        // Regression pin for https://github.com/anthropics/buffa/issues/88:
419        // Any.value is bytes::Bytes (not Vec<u8>), so derive(Arbitrary) on Any
420        // requires the ::buffa::__private::arbitrary_bytes shim.
421        let raw = [0u8; 64];
422        let mut u = Unstructured::new(&raw);
423        let any = Any::arbitrary(&mut u).unwrap();
424        let _ = any.value.slice(..);
425    }
426
427    /// Test double whose `compute_size` reports over the 2 GiB limit and
428    /// whose `write_to` writes nothing — exercises `pack`'s guard without
429    /// materializing gigabytes. Mirrors buffa's crate-internal
430    /// `test_doubles::SizedMsg` (`#[cfg(test)]` items don't cross the crate
431    /// boundary).
432    #[derive(Clone, Default, PartialEq, Debug)]
433    struct HugeMsg;
434
435    impl buffa::DefaultInstance for HugeMsg {
436        fn default_instance() -> &'static Self {
437            static INST: buffa::__private::OnceBox<HugeMsg> = buffa::__private::OnceBox::new();
438            INST.get_or_init(|| alloc::boxed::Box::new(HugeMsg))
439        }
440    }
441
442    impl buffa::Message for HugeMsg {
443        fn compute_size(&self, _cache: &mut buffa::SizeCache) -> u32 {
444            buffa::MAX_MESSAGE_BYTES + 1
445        }
446        fn write_to(&self, _cache: &mut buffa::SizeCache, _buf: &mut impl buffa::EncodeSink) {}
447        fn merge_field(
448            &mut self,
449            tag: buffa::encoding::Tag,
450            buf: &mut impl bytes::Buf,
451            _ctx: buffa::DecodeContext<'_>,
452        ) -> Result<(), buffa::DecodeError> {
453            buffa::encoding::skip_field(tag, buf)?;
454            Ok(())
455        }
456        fn clear(&mut self) {}
457    }
458
459    #[test]
460    fn try_pack_over_limit_errs() {
461        assert_eq!(
462            Any::try_pack(&HugeMsg, "type.googleapis.com/x"),
463            Err(buffa::EncodeError::MessageTooLarge)
464        );
465    }
466
467    #[test]
468    #[should_panic(expected = "2 GiB protobuf limit")]
469    fn pack_over_limit_panics() {
470        let _ = Any::pack(&HugeMsg, "type.googleapis.com/x");
471    }
472
473    #[test]
474    fn try_pack_matches_pack_for_normal_messages() {
475        let ts = Timestamp {
476            seconds: 42,
477            ..Default::default()
478        };
479        let url = "type.googleapis.com/google.protobuf.Timestamp";
480        assert_eq!(Any::try_pack(&ts, url).unwrap(), Any::pack(&ts, url));
481    }
482
483    #[test]
484    fn pack_and_unpack() {
485        let ts = Timestamp {
486            seconds: 1_000_000_000,
487            nanos: 0,
488            ..Default::default()
489        };
490        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
491        assert_eq!(
492            any.type_url(),
493            "type.googleapis.com/google.protobuf.Timestamp"
494        );
495
496        let decoded: Timestamp = any.unpack_unchecked().unwrap();
497        assert_eq!(decoded, ts);
498    }
499
500    #[test]
501    fn unpack_if_matching() {
502        let ts = Timestamp {
503            seconds: 42,
504            ..Default::default()
505        };
506        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
507
508        let result: Option<Timestamp> = any
509            .unpack_if("type.googleapis.com/google.protobuf.Timestamp")
510            .unwrap();
511        assert_eq!(result, Some(ts));
512    }
513
514    #[test]
515    fn unpack_if_wrong_type_returns_none() {
516        let ts = Timestamp {
517            seconds: 42,
518            ..Default::default()
519        };
520        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
521
522        let result: Option<Timestamp> = any
523            .unpack_if("type.googleapis.com/google.protobuf.Duration")
524            .unwrap();
525        assert!(result.is_none());
526    }
527
528    #[test]
529    fn clone_shares_payload_buffer() {
530        let orig = Any {
531            type_url: "type.googleapis.com/example.Msg".into(),
532            value: alloc::vec![0xAB; 1024].into(),
533            ..Default::default()
534        };
535        let dup = orig.clone();
536        assert_eq!(orig.value.as_ptr(), dup.value.as_ptr());
537        assert_eq!(orig.value.len(), dup.value.len());
538    }
539
540    #[test]
541    fn is_type() {
542        let ts = Timestamp::default();
543        let any = Any::pack(&ts, "type.googleapis.com/google.protobuf.Timestamp");
544        assert!(any.is_type("type.googleapis.com/google.protobuf.Timestamp"));
545        assert!(!any.is_type("type.googleapis.com/google.protobuf.Duration"));
546    }
547
548    #[test]
549    fn round_trip_encoding() {
550        let ts = Timestamp {
551            seconds: 99,
552            nanos: 1,
553            ..Default::default()
554        };
555        let any = Any::pack(&ts, "test");
556
557        let bytes = any.encode_to_vec();
558        let decoded_any = Any::decode(&mut bytes.as_slice()).unwrap();
559        let decoded_ts: Timestamp = decoded_any.unpack_unchecked().unwrap();
560        assert_eq!(decoded_ts, ts);
561    }
562
563    #[cfg(feature = "json")]
564    mod serde_tests {
565        use super::*;
566        use crate::google::protobuf::Duration;
567        use buffa::any_registry::clear_any_registry;
568        use buffa::type_registry::{clear_text_registry, set_type_registry, TypeRegistry};
569
570        /// Mutex to serialize tests that manipulate the global registries.
571        /// Each test binary needs its own lock since #[cfg(test)] modules
572        /// cannot be shared across crates.
573        static REGISTRY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
574
575        fn with_registry<R>(f: impl FnOnce() -> R) -> R {
576            let _guard = REGISTRY_LOCK.lock().unwrap();
577            let mut reg = TypeRegistry::new();
578            register_wkt_types(&mut reg);
579            set_type_registry(reg);
580            let result = f();
581            clear_any_registry();
582            clear_text_registry();
583            result
584        }
585
586        fn without_registry<R>(f: impl FnOnce() -> R) -> R {
587            let _guard = REGISTRY_LOCK.lock().unwrap();
588            clear_any_registry();
589            clear_text_registry();
590            f()
591        }
592
593        // ── TextFormat impl (Any expansion) ─────────────────────────────────
594        //
595        // Here rather than in `text_tests` because these manipulate the
596        // same global `AtomicPtr` as the JSON tests above — both must
597        // serialize on `REGISTRY_LOCK`.
598
599        #[test]
600        fn text_registry_roundtrip_wkt() {
601            use crate::google::protobuf::Empty;
602            use buffa::text::{decode_from_str, encode_to_string};
603            with_registry(|| {
604                // register_wkt_types installs Empty with text fn-ptrs.
605                let any = Any::pack(&Empty::default(), Empty::TYPE_URL);
606                let text = encode_to_string(&any);
607                // Empty has no fields → `{}`.
608                assert_eq!(text, "[type.googleapis.com/google.protobuf.Empty] {}");
609
610                let back: Any = decode_from_str(&text).unwrap();
611                assert_eq!(back.type_url, Empty::TYPE_URL);
612                assert_eq!(back.value, alloc::vec::Vec::<u8>::new());
613            });
614        }
615
616        #[test]
617        fn text_unregistered_url_errors_on_decode() {
618            use buffa::text::decode_from_str;
619            // Registry installed but URL not in it — the
620            // `AnyFieldWithInvalidType` conformance shape.
621            with_registry(|| {
622                let result: Result<Any, _> =
623                    decode_from_str("[type.googleapis.com/unknown.Type] { x: 1 }");
624                assert!(result.is_err(), "unknown URL should error, not skip");
625            });
626        }
627
628        #[test]
629        fn text_bracket_without_registry_errors() {
630            use buffa::text::decode_from_str;
631            // No registry at all → bracket name is a registry miss → error.
632            without_registry(|| {
633                let result: Result<Any, _> = decode_from_str("[type.example.com/Unknown] { x: 1 }");
634                assert!(result.is_err());
635            });
636        }
637
638        #[test]
639        fn serialize_wkt_uses_value_wrapping() {
640            with_registry(|| {
641                let ts = Timestamp {
642                    seconds: 1_000_000_000,
643                    nanos: 0,
644                    ..Default::default()
645                };
646                let any = Any::pack(&ts, Timestamp::TYPE_URL);
647                let json = serde_json::to_value(&any).unwrap();
648                assert_eq!(json["@type"], Timestamp::TYPE_URL);
649                assert_eq!(json["value"], "2001-09-09T01:46:40Z");
650            });
651        }
652
653        #[test]
654        fn serialize_duration_wkt() {
655            with_registry(|| {
656                let dur = Duration::from_secs_nanos(1, 500_000_000);
657                let any = Any::pack(&dur, Duration::TYPE_URL);
658                let json = serde_json::to_value(&any).unwrap();
659                assert_eq!(json["@type"], Duration::TYPE_URL);
660                assert_eq!(json["value"], "1.500s");
661            });
662        }
663
664        #[test]
665        fn serialize_empty_any_is_empty_object() {
666            with_registry(|| {
667                let any = Any::default();
668                let json = serde_json::to_string(&any).unwrap();
669                assert_eq!(json, "{}");
670            });
671        }
672
673        #[test]
674        fn deserialize_wkt_from_json() {
675            with_registry(|| {
676                let json = r#"{
677                    "@type": "type.googleapis.com/google.protobuf.Duration",
678                    "value": "1.5s"
679                }"#;
680                let any: Any = serde_json::from_str(json).unwrap();
681                assert_eq!(any.type_url, Duration::TYPE_URL);
682
683                let dur: Duration = any.unpack_unchecked().unwrap();
684                assert_eq!(dur.seconds, 1);
685                assert_eq!(dur.nanos, 500_000_000);
686            });
687        }
688
689        #[test]
690        fn deserialize_unordered_type_tag() {
691            with_registry(|| {
692                // @type appears after the value field.
693                let json = r#"{
694                    "value": "1.5s",
695                    "@type": "type.googleapis.com/google.protobuf.Duration"
696                }"#;
697                let any: Any = serde_json::from_str(json).unwrap();
698                assert_eq!(any.type_url, Duration::TYPE_URL);
699
700                let dur: Duration = any.unpack_unchecked().unwrap();
701                assert_eq!(dur.seconds, 1);
702                assert_eq!(dur.nanos, 500_000_000);
703            });
704        }
705
706        #[test]
707        fn roundtrip_wkt_json() {
708            with_registry(|| {
709                let ts = Timestamp {
710                    seconds: 1_000_000_000,
711                    nanos: 0,
712                    ..Default::default()
713                };
714                let any = Any::pack(&ts, Timestamp::TYPE_URL);
715                let json = serde_json::to_string(&any).unwrap();
716                let decoded: Any = serde_json::from_str(&json).unwrap();
717                let decoded_ts: Timestamp = decoded.unpack_unchecked().unwrap();
718                assert_eq!(decoded_ts, ts);
719            });
720        }
721
722        #[test]
723        fn nested_any_roundtrip() {
724            with_registry(|| {
725                let dur = Duration::from_secs(42);
726                let inner_any = Any::pack(&dur, Duration::TYPE_URL);
727                let outer_any = Any::pack(&inner_any, Any::TYPE_URL);
728
729                let json = serde_json::to_string(&outer_any).unwrap();
730                let decoded_outer: Any = serde_json::from_str(&json).unwrap();
731                let decoded_inner: Any = decoded_outer.unpack_unchecked().unwrap();
732                let decoded_dur: Duration = decoded_inner.unpack_unchecked().unwrap();
733                assert_eq!(decoded_dur.seconds, 42);
734            });
735        }
736
737        #[test]
738        fn fallback_base64_without_registry() {
739            without_registry(|| {
740                let any = Any {
741                    type_url: "type.googleapis.com/unknown.Type".into(),
742                    value: vec![0x08, 0x96, 0x01].into(),
743                    ..Default::default()
744                };
745                let json = serde_json::to_string(&any).unwrap();
746                assert!(json.contains("@type"));
747                assert!(json.contains("value"));
748
749                let decoded: Any = serde_json::from_str(&json).unwrap();
750                assert_eq!(decoded.type_url, any.type_url);
751                assert_eq!(decoded.value, any.value);
752            });
753        }
754
755        #[test]
756        fn deserialize_missing_type_returns_default() {
757            let json = r#"{}"#;
758            let any: Any = serde_json::from_str(json).unwrap();
759            assert_eq!(any, Any::default());
760        }
761
762        #[test]
763        fn fallback_base64_with_registry_but_unknown_type() {
764            with_registry(|| {
765                let any = Any {
766                    type_url: "type.googleapis.com/unknown.Type".into(),
767                    value: vec![0x08, 0x96, 0x01].into(),
768                    ..Default::default()
769                };
770                let json = serde_json::to_string(&any).unwrap();
771                let decoded: Any = serde_json::from_str(&json).unwrap();
772                assert_eq!(decoded.type_url, any.type_url);
773                assert_eq!(decoded.value, any.value);
774            });
775        }
776
777        #[test]
778        fn deserialize_rejects_empty_type_url() {
779            let json = r#"{"@type": "", "value": ""}"#;
780            let err = serde_json::from_str::<Any>(json).unwrap_err();
781            assert!(err.to_string().contains("valid type URL"), "{err}");
782        }
783
784        #[test]
785        fn deserialize_rejects_type_url_without_slash() {
786            let json = r#"{"@type": "not_a_url", "value": ""}"#;
787            let err = serde_json::from_str::<Any>(json).unwrap_err();
788            assert!(err.to_string().contains("valid type URL"), "{err}");
789        }
790
791        // ── Non-WKT registered type (fields inlined at top level) ─────
792        // WKTs use {"@type": ..., "value": <json>} wrapping.
793        // Regular messages use {"@type": ..., "field1": ..., "field2": ...}.
794        // Previously only the WKT path was tested.
795
796        /// Hand-written to_json: decode the Any bytes as a single varint
797        /// field (number=1), return it as a JSON object {"id": N}.
798        fn user_type_to_json(bytes: &[u8]) -> Result<serde_json::Value, String> {
799            use buffa::encoding::Tag;
800            let mut cur = bytes;
801            let mut id = 0i64;
802            while !cur.is_empty() {
803                let tag = Tag::decode(&mut cur).map_err(|e| e.to_string())?;
804                if tag.field_number() == 1 {
805                    id =
806                        buffa::encoding::decode_varint(&mut cur).map_err(|e| e.to_string())? as i64;
807                } else {
808                    buffa::encoding::skip_field(tag, &mut cur).map_err(|e| e.to_string())?;
809                }
810            }
811            Ok(serde_json::json!({ "id": id }))
812        }
813
814        /// Hand-written from_json: extract {"id": N}, encode as varint field 1.
815        fn user_type_from_json(value: serde_json::Value) -> Result<alloc::vec::Vec<u8>, String> {
816            use buffa::encoding::{encode_varint, Tag, WireType};
817            let id = value
818                .get("id")
819                .and_then(|v| v.as_i64())
820                .ok_or_else(|| "missing or invalid 'id' field".to_string())?;
821            let mut buf = alloc::vec::Vec::new();
822            Tag::new(1, WireType::Varint).encode(&mut buf);
823            encode_varint(id as u64, &mut buf);
824            Ok(buf)
825        }
826
827        fn with_user_type_registry<R>(f: impl FnOnce() -> R) -> R {
828            use buffa::type_registry::JsonAnyEntry;
829            let _guard = REGISTRY_LOCK.lock().unwrap();
830            let mut reg = TypeRegistry::new();
831            // Register as NON-WKT (is_wkt=false) — fields inline at top level.
832            reg.register_json_any(JsonAnyEntry {
833                type_url: "type.example.com/user.Thing",
834                to_json: user_type_to_json,
835                from_json: user_type_from_json,
836                is_wkt: false,
837            });
838            set_type_registry(reg);
839            let result = f();
840            clear_any_registry();
841            clear_text_registry();
842            result
843        }
844
845        #[test]
846        fn serialize_non_wkt_inlines_fields() {
847            with_user_type_registry(|| {
848                // Encode {id: 42} as proto wire bytes.
849                let any = Any {
850                    type_url: "type.example.com/user.Thing".into(),
851                    // field 1, varint 42: tag=0x08, value=0x2A
852                    value: vec![0x08, 0x2A].into(),
853                    ..Default::default()
854                };
855
856                let json = serde_json::to_value(&any).unwrap();
857                // Non-WKT format: fields at top level alongside @type.
858                assert_eq!(json["@type"], "type.example.com/user.Thing");
859                assert_eq!(json["id"], 42);
860                // Should NOT have a "value" wrapper key.
861                assert!(
862                    json.get("value").is_none(),
863                    "non-WKT should not use 'value' wrapping: {json}"
864                );
865            });
866        }
867
868        #[test]
869        fn deserialize_non_wkt_from_inlined_fields() {
870            with_user_type_registry(|| {
871                let json = r#"{
872                    "@type": "type.example.com/user.Thing",
873                    "id": 99
874                }"#;
875                let any: Any = serde_json::from_str(json).unwrap();
876                assert_eq!(any.type_url, "type.example.com/user.Thing");
877                // Verify the from_json encoded it back to wire bytes.
878                assert_eq!(any.value, vec![0x08, 99]);
879            });
880        }
881
882        #[test]
883        fn non_wkt_round_trip() {
884            with_user_type_registry(|| {
885                let original = Any {
886                    type_url: "type.example.com/user.Thing".into(),
887                    value: vec![0x08, 0x07].into(), // id=7
888                    ..Default::default()
889                };
890                let json = serde_json::to_string(&original).unwrap();
891                let decoded: Any = serde_json::from_str(&json).unwrap();
892                assert_eq!(decoded.type_url, original.type_url);
893                assert_eq!(decoded.value, original.value);
894            });
895        }
896
897        #[test]
898        fn serialize_non_wkt_rejects_non_object_json() {
899            // If to_json for a non-WKT type returns something other than a
900            // JSON object, serialization must fail (can't inline non-object
901            // fields alongside @type).
902            use buffa::type_registry::JsonAnyEntry;
903            let _guard = REGISTRY_LOCK.lock().unwrap();
904            let mut reg = TypeRegistry::new();
905            reg.register_json_any(JsonAnyEntry {
906                type_url: "type.example.com/user.BadType",
907                to_json: |_bytes| Ok(serde_json::Value::Number(42.into())),
908                from_json: |_v| Ok(alloc::vec::Vec::new()),
909                is_wkt: false,
910            });
911            set_type_registry(reg);
912
913            let any = Any {
914                type_url: "type.example.com/user.BadType".into(),
915                value: vec![].into(),
916                ..Default::default()
917            };
918            let result = serde_json::to_string(&any);
919            clear_any_registry();
920            clear_text_registry();
921            assert!(result.is_err(), "expected error for non-object to_json");
922            assert!(
923                result
924                    .unwrap_err()
925                    .to_string()
926                    .contains("must return a JSON object"),
927                "wrong error message"
928            );
929        }
930
931        #[test]
932        fn deserialize_rejects_non_string_type() {
933            // @type as a non-string value → error.
934            let json = r#"{"@type": 123}"#;
935            let err = serde_json::from_str::<Any>(json).unwrap_err();
936            assert!(err.to_string().contains("@type must be a string"), "{err}");
937        }
938    }
939}