Skip to main content

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}