Skip to content

Commit 7ce9b97

Browse files
feat: Name trait + Any encoding support (#896)
* feat: `Name` trait + `Any` encoding support As discussed in #299 and #858, adds a `Name` trait which associates a type name and package constants with a `Message` type. It also provides `full_name` and `type_url` methods. The `type_url` method is used by newly added methods on the `Any` type which can be used for decoding/encoding messages: - `Any::from_msg`: encodes a given `Message`, returning `Any`. - `Any::to_msg`: decodes `Any::value` as the given `Message`, first validating the message type has the expected type URL. * Add private `TypeUrl` type Implements the basic rules for parsing type URLs as documented in: https://github.com/protocolbuffers/protobuf/blob/a281c13/src/google/protobuf/any.proto#L129C2-L156C50 Notably this extracts the final path segment of the URL which contains the full name of the type, and uses that for type comparisons. * CI: bump test toolchain to 1.64 This is the MSRV of `petgraph` now: error: package `petgraph v0.6.4` cannot be built because it requires rustc 1.64 or newer, while the currently active rustc version is 1.63.0 * Add `Name` impls for well-known protobuf types Also adds tests for `Any::{from_msg, to_msg}`. * Fix no_std --------- Co-authored-by: Lucio Franco <[email protected]>
1 parent f9a3cff commit 7ce9b97

File tree

3 files changed

+185
-0
lines changed

3 files changed

+185
-0
lines changed

prost-types/src/lib.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ use core::i64;
2424
use core::str::FromStr;
2525
use core::time;
2626

27+
use prost::alloc::format;
28+
use prost::alloc::string::String;
29+
use prost::alloc::vec::Vec;
30+
use prost::{DecodeError, EncodeError, Message, Name};
31+
2732
pub use protobuf::*;
2833

2934
// The Protobuf `Duration` and `Timestamp` types can't delegate to the standard library equivalents
@@ -33,6 +38,58 @@ pub use protobuf::*;
3338
const NANOS_PER_SECOND: i32 = 1_000_000_000;
3439
const NANOS_MAX: i32 = NANOS_PER_SECOND - 1;
3540

41+
const PACKAGE: &str = "google.protobuf";
42+
43+
impl Any {
44+
/// Serialize the given message type `M` as [`Any`].
45+
pub fn from_msg<M>(msg: &M) -> Result<Self, EncodeError>
46+
where
47+
M: Name,
48+
{
49+
let type_url = M::type_url();
50+
let mut value = Vec::new();
51+
Message::encode(msg, &mut value)?;
52+
Ok(Any { type_url, value })
53+
}
54+
55+
/// Decode the given message type `M` from [`Any`], validating that it has
56+
/// the expected type URL.
57+
pub fn to_msg<M>(&self) -> Result<M, DecodeError>
58+
where
59+
M: Default + Name + Sized,
60+
{
61+
let expected_type_url = M::type_url();
62+
63+
match (
64+
TypeUrl::new(&expected_type_url),
65+
TypeUrl::new(&self.type_url),
66+
) {
67+
(Some(expected), Some(actual)) => {
68+
if expected == actual {
69+
return Ok(M::decode(&*self.value)?);
70+
}
71+
}
72+
_ => (),
73+
}
74+
75+
let mut err = DecodeError::new(format!(
76+
"expected type URL: \"{}\" (got: \"{}\")",
77+
expected_type_url, &self.type_url
78+
));
79+
err.push("unexpected type URL", "type_url");
80+
Err(err)
81+
}
82+
}
83+
84+
impl Name for Any {
85+
const PACKAGE: &'static str = PACKAGE;
86+
const NAME: &'static str = "Any";
87+
88+
fn type_url() -> String {
89+
type_url_for::<Self>()
90+
}
91+
}
92+
3693
impl Duration {
3794
/// Normalizes the duration to a canonical format.
3895
///
@@ -85,6 +142,15 @@ impl Duration {
85142
}
86143
}
87144

145+
impl Name for Duration {
146+
const PACKAGE: &'static str = PACKAGE;
147+
const NAME: &'static str = "Duration";
148+
149+
fn type_url() -> String {
150+
type_url_for::<Self>()
151+
}
152+
}
153+
88154
impl TryFrom<time::Duration> for Duration {
89155
type Error = DurationError;
90156

@@ -298,6 +364,15 @@ impl Timestamp {
298364
}
299365
}
300366

367+
impl Name for Timestamp {
368+
const PACKAGE: &'static str = PACKAGE;
369+
const NAME: &'static str = "Timestamp";
370+
371+
fn type_url() -> String {
372+
type_url_for::<Self>()
373+
}
374+
}
375+
301376
/// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`.
302377
/// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`.
303378
#[cfg(feature = "std")]
@@ -421,6 +496,49 @@ impl fmt::Display for Timestamp {
421496
}
422497
}
423498

499+
/// URL/resource name that uniquely identifies the type of the serialized protocol buffer message,
500+
/// e.g. `type.googleapis.com/google.protobuf.Duration`.
501+
///
502+
/// This string must contain at least one "/" character.
503+
///
504+
/// The last segment of the URL's path must represent the fully qualified name of the type (as in
505+
/// `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading "." is
506+
/// not accepted).
507+
///
508+
/// If no scheme is provided, `https` is assumed.
509+
///
510+
/// Schemes other than `http`, `https` (or the empty scheme) might be used with implementation
511+
/// specific semantics.
512+
#[derive(Debug, Eq, PartialEq)]
513+
struct TypeUrl<'a> {
514+
/// Fully qualified name of the type, e.g. `google.protobuf.Duration`
515+
full_name: &'a str,
516+
}
517+
518+
impl<'a> TypeUrl<'a> {
519+
fn new(s: &'a str) -> core::option::Option<Self> {
520+
// Must contain at least one "/" character.
521+
let slash_pos = s.rfind('/')?;
522+
523+
// The last segment of the URL's path must represent the fully qualified name
524+
// of the type (as in `path/google.protobuf.Duration`)
525+
let full_name = s.get((slash_pos + 1)..)?;
526+
527+
// The name should be in a canonical form (e.g., leading "." is not accepted).
528+
if full_name.starts_with('.') {
529+
return None;
530+
}
531+
532+
Some(Self { full_name })
533+
}
534+
}
535+
536+
/// Compute the type URL for the given `google.protobuf` type, using `type.googleapis.com` as the
537+
/// authority for the URL.
538+
fn type_url_for<T: Name>() -> String {
539+
format!("type.googleapis.com/{}.{}", T::PACKAGE, T::NAME)
540+
}
541+
424542
#[cfg(test)]
425543
mod tests {
426544
use super::*;
@@ -744,4 +862,41 @@ mod tests {
744862
);
745863
}
746864
}
865+
866+
#[test]
867+
fn check_any_serialization() {
868+
let message = Timestamp::date(2000, 01, 01).unwrap();
869+
let any = Any::from_msg(&message).unwrap();
870+
assert_eq!(
871+
&any.type_url,
872+
"type.googleapis.com/google.protobuf.Timestamp"
873+
);
874+
875+
let message2 = any.to_msg::<Timestamp>().unwrap();
876+
assert_eq!(message, message2);
877+
878+
// Wrong type URL
879+
assert!(any.to_msg::<Duration>().is_err());
880+
}
881+
882+
#[test]
883+
fn check_type_url_parsing() {
884+
let example_type_name = "google.protobuf.Duration";
885+
886+
let url = TypeUrl::new("type.googleapis.com/google.protobuf.Duration").unwrap();
887+
assert_eq!(url.full_name, example_type_name);
888+
889+
let full_url =
890+
TypeUrl::new("https://type.googleapis.com/google.protobuf.Duration").unwrap();
891+
assert_eq!(full_url.full_name, example_type_name);
892+
893+
let relative_url = TypeUrl::new("/google.protobuf.Duration").unwrap();
894+
assert_eq!(relative_url.full_name, example_type_name);
895+
896+
// The name should be in a canonical form (e.g., leading "." is not accepted).
897+
assert_eq!(TypeUrl::new("/.google.protobuf.Duration"), None);
898+
899+
// Must contain at least one "/" character.
900+
assert_eq!(TypeUrl::new("google.protobuf.Duration"), None);
901+
}
747902
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ pub use bytes;
1111

1212
mod error;
1313
mod message;
14+
mod name;
1415
mod types;
1516

1617
#[doc(hidden)]
1718
pub mod encoding;
1819

1920
pub use crate::error::{DecodeError, EncodeError};
2021
pub use crate::message::Message;
22+
pub use crate::name::Name;
2123

2224
use bytes::{Buf, BufMut};
2325

src/name.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Support for associating type name information with a [`Message`].
2+
3+
use crate::Message;
4+
use alloc::{format, string::String};
5+
6+
/// Associate a type name with a [`Message`] type.
7+
pub trait Name: Message {
8+
/// Type name for this [`Message`]. This is the camel case name,
9+
/// e.g. `TypeName`.
10+
const NAME: &'static str;
11+
12+
/// Package name this message type is contained in. They are domain-like
13+
/// and delimited by `.`, e.g. `google.protobuf`.
14+
const PACKAGE: &'static str;
15+
16+
/// Full name of this message type containing both the package name and
17+
/// type name, e.g. `google.protobuf.TypeName`.
18+
fn full_name() -> String {
19+
format!("{}.{}", Self::NAME, Self::PACKAGE)
20+
}
21+
22+
/// Type URL for this message, which by default is the full name with a
23+
/// leading slash, but may also include a leading domain name, e.g.
24+
/// `type.googleapis.com/google.profile.Person`.
25+
fn type_url() -> String {
26+
format!("/{}", Self::full_name())
27+
}
28+
}

0 commit comments

Comments
 (0)