oxiproto_reflect/lib.rs
1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3//! Runtime protobuf reflection via prost-reflect.
4//!
5//! This crate provides a thin facade over [`prost_reflect`] for dynamic
6//! protobuf operations: building a [`DescriptorPool`] from a
7//! [`prost_types::FileDescriptorSet`] and constructing [`DynamicMessage`]
8//! instances at runtime without generated Rust types.
9//!
10//! ## Quick start
11//!
12//! ```rust,no_run
13//! use oxiproto_reflect::{pool_from_fds_bytes, dynamic_message};
14//! # use prost_reflect::ReflectMessage;
15//!
16//! // `fds_bytes` is the raw bytes of a `FileDescriptorSet` proto.
17//! # fn example(fds_bytes: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
18//! let pool = pool_from_fds_bytes(fds_bytes)?;
19//! let msg = dynamic_message(&pool, "my.package.MyMessage")?;
20//! println!("fields: {:?}", msg.descriptor().fields().collect::<Vec<_>>());
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! ## Debug and Display for DynamicMessage
26//!
27//! [`DynamicMessage`] implements both [`std::fmt::Debug`] and
28//! [`std::fmt::Display`] (protobuf text format). The following example
29//! verifies both traits work correctly through this crate's re-exports.
30//!
31//! ```rust
32//! use oxiproto_reflect::{pool_from_fds, DynamicMessage};
33//! use prost_types::{
34//! FileDescriptorSet, FileDescriptorProto, DescriptorProto, FieldDescriptorProto,
35//! };
36//! use prost_types::field_descriptor_proto::{Label, Type};
37//!
38//! let fds = FileDescriptorSet {
39//! file: vec![FileDescriptorProto {
40//! name: Some("test.proto".to_string()),
41//! syntax: Some("proto3".to_string()),
42//! message_type: vec![DescriptorProto {
43//! name: Some("Ping".to_string()),
44//! field: vec![FieldDescriptorProto {
45//! name: Some("value".to_string()),
46//! number: Some(1),
47//! label: Some(Label::Optional as i32),
48//! r#type: Some(Type::Int32 as i32),
49//! json_name: Some("value".to_string()),
50//! ..Default::default()
51//! }],
52//! ..Default::default()
53//! }],
54//! ..Default::default()
55//! }],
56//! };
57//!
58//! let pool = pool_from_fds(fds).unwrap();
59//! let msg_desc = pool.get_message_by_name("Ping").unwrap();
60//! let msg = DynamicMessage::new(msg_desc);
61//!
62//! // Debug format is always available.
63//! let debug_str = format!("{msg:?}");
64//! assert!(!debug_str.is_empty());
65//!
66//! // Display uses the protobuf text format; an empty message formats to "".
67//! let display_str = format!("{msg}");
68//! assert_eq!(display_str, "");
69//! ```
70
71pub use prost_reflect::{
72 DescriptorPool, DynamicMessage, EnumDescriptor, FieldDescriptor, FileDescriptor,
73 MessageDescriptor, MethodDescriptor, ServiceDescriptor, UnknownField,
74};
75
76/// Re-export of [`prost_reflect::Value`] under a distinct alias to avoid
77/// name conflicts with [`prost_types::Value`].
78pub use prost_reflect::Value as ReflectValue;
79
80/// Re-export of the [`prost_reflect::ReflectMessage`] trait so callers can
81/// use `msg.descriptor()` without a separate `prost_reflect` dependency.
82pub use prost_reflect::ReflectMessage;
83
84pub mod dynamic;
85
86pub use dynamic::{clear_field, get_field_by_name, has_field, set_field_by_name, unknown_fields};
87
88pub mod native;
89
90// Re-export the native reflection types under a `Native`-prefixed alias so they
91// coexist with the `prost-reflect`-backed types re-exported above (which keep
92// their canonical names for backwards compatibility). The full, unprefixed
93// names are also available via the `native` module path, e.g.
94// `oxiproto_reflect::native::DescriptorPool`.
95pub use native::{
96 Cardinality as NativeCardinality, DescriptorPool as NativeDescriptorPool,
97 DynamicMessage as NativeDynamicMessage, EnumDescriptor as NativeEnumDescriptor,
98 EnumValueDescriptor as NativeEnumValueDescriptor, FieldDescriptor as NativeFieldDescriptor,
99 FileDescriptor as NativeFileDescriptor, Kind as NativeKind, MapKey as NativeMapKey,
100 MessageDescriptor as NativeMessageDescriptor, MethodDescriptor as NativeMethodDescriptor,
101 NativeJsonError, NativeTextError, OneofDescriptor as NativeOneofDescriptor,
102 ServiceDescriptor as NativeServiceDescriptor, Value as NativeValue,
103};
104
105use prost::Message;
106use prost_types::FileDescriptorSet;
107
108/// Errors produced by reflection operations.
109#[derive(Debug)]
110pub enum ReflectError {
111 /// Failed to decode the raw bytes as a `FileDescriptorSet`.
112 Decode(prost::DecodeError),
113 /// The descriptor pool could not be constructed from the provided descriptors.
114 Pool(String),
115 /// A named symbol (message, service, enum, field) was not found in the pool.
116 NotFound(String),
117 /// Field name or type error during dynamic message access.
118 Field(String),
119}
120
121impl std::fmt::Display for ReflectError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 match self {
124 ReflectError::Decode(e) => write!(f, "failed to decode FileDescriptorSet: {e}"),
125 ReflectError::Pool(e) => write!(f, "failed to build DescriptorPool: {e}"),
126 ReflectError::NotFound(name) => write!(f, "'{name}' not found in pool"),
127 ReflectError::Field(msg) => write!(f, "field error: {msg}"),
128 }
129 }
130}
131
132impl std::error::Error for ReflectError {
133 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134 match self {
135 ReflectError::Decode(e) => Some(e),
136 ReflectError::Pool(_) | ReflectError::NotFound(_) | ReflectError::Field(_) => None,
137 }
138 }
139}
140
141impl From<oxiproto_core::OxiProtoError> for ReflectError {
142 fn from(e: oxiproto_core::OxiProtoError) -> Self {
143 ReflectError::Pool(e.to_string())
144 }
145}
146
147impl From<ReflectError> for oxiproto_core::OxiProtoError {
148 fn from(e: ReflectError) -> Self {
149 oxiproto_core::OxiProtoError::ParseError(e.to_string())
150 }
151}
152
153/// Build a [`DescriptorPool`] from the raw bytes of a serialized
154/// [`prost_types::FileDescriptorSet`].
155///
156/// The bytes are typically produced at build time by
157/// `prost_build::Config::file_descriptor_set_path`, or constructed
158/// programmatically in tests.
159///
160/// # Errors
161///
162/// Returns [`ReflectError::Decode`] if `fds_bytes` cannot be decoded as a
163/// `FileDescriptorSet`, or [`ReflectError::Pool`] if the pool construction
164/// fails (e.g. missing imports or invalid descriptors).
165pub fn pool_from_fds_bytes(fds_bytes: &[u8]) -> Result<DescriptorPool, ReflectError> {
166 let fds = FileDescriptorSet::decode(fds_bytes).map_err(ReflectError::Decode)?;
167 DescriptorPool::from_file_descriptor_set(fds).map_err(|e| ReflectError::Pool(e.to_string()))
168}
169
170/// Build a [`DescriptorPool`] directly from a [`FileDescriptorSet`].
171///
172/// Unlike [`pool_from_fds_bytes`], this function accepts the already-decoded
173/// struct and avoids the bytes round-trip.
174///
175/// # Errors
176///
177/// Returns [`ReflectError::Pool`] if the pool construction fails (e.g. missing
178/// imports or invalid descriptors).
179pub fn pool_from_fds(fds: FileDescriptorSet) -> Result<DescriptorPool, ReflectError> {
180 DescriptorPool::from_file_descriptor_set(fds).map_err(|e| ReflectError::Pool(e.to_string()))
181}
182
183/// Construct an empty [`DynamicMessage`] for the named message in `pool`.
184///
185/// `full_name` must be the fully-qualified message name, e.g.
186/// `"my.package.MyMessage"`.
187///
188/// # Errors
189///
190/// Returns [`ReflectError::NotFound`] if `full_name` does not exist in `pool`.
191pub fn dynamic_message(
192 pool: &DescriptorPool,
193 full_name: &str,
194) -> Result<DynamicMessage, ReflectError> {
195 let msg_desc = pool
196 .get_message_by_name(full_name)
197 .ok_or_else(|| ReflectError::NotFound(full_name.to_owned()))?;
198 Ok(DynamicMessage::new(msg_desc))
199}
200
201/// Look up a service descriptor by its fully-qualified name.
202///
203/// Returns `None` if no service with that name exists in `pool`.
204pub fn get_service_by_name(pool: &DescriptorPool, full_name: &str) -> Option<ServiceDescriptor> {
205 pool.get_service_by_name(full_name)
206}
207
208/// Look up an enum descriptor by its fully-qualified name.
209///
210/// Returns `None` if no enum with that name exists in `pool`.
211pub fn get_enum_by_name(pool: &DescriptorPool, full_name: &str) -> Option<EnumDescriptor> {
212 pool.get_enum_by_name(full_name)
213}
214
215/// Iterate over all message descriptors registered in the pool.
216///
217/// Includes nested messages defined inside other messages.
218pub fn all_messages(pool: &DescriptorPool) -> impl Iterator<Item = MessageDescriptor> + '_ {
219 pool.all_messages()
220}
221
222/// Iterate over all service descriptors registered in the pool.
223///
224/// Forwards to `DescriptorPool::services()` which is the equivalent iterator
225/// on prost-reflect 0.16.x (there is no `all_services` method; all services
226/// are top-level by definition in protobuf).
227pub fn all_services(pool: &DescriptorPool) -> impl Iterator<Item = ServiceDescriptor> + '_ {
228 pool.services()
229}