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