Skip to main content

prost_protovalidate_types/
lib.rs

1//! Generated Rust types for the [`buf.validate`](https://github.com/bufbuild/protovalidate)
2//! protobuf schema, built with `prost` and `prost-reflect`.
3//!
4//! This crate provides:
5//!
6//! - All message and enum types from `buf/validate/validate.proto`
7//!   (e.g. [`FieldRules`], [`MessageRules`], [`OneofRules`]).
8//! - A shared [`DESCRIPTOR_POOL`] containing the file descriptor set for
9//!   runtime reflection.
10//! - Extension traits for extracting constraint annotations from descriptors:
11//!   - [`FieldConstraintsExt`] — `buf.validate.field` rules on a
12//!     [`FieldDescriptor`].
13//!   - [`MessageConstraintsExt`] — `buf.validate.message` rules on a
14//!     [`MessageDescriptor`].
15//!   - [`OneofConstraintsExt`] — `buf.validate.oneof` rules on a
16//!     [`OneofDescriptor`].
17//!   - [`PredefinedConstraintsExt`] — `buf.validate.predefined` rules.
18//!   - [`FieldConstraintsDynExt`] / [`MessageConstraintsDynExt`] — raw
19//!     [`DynamicMessage`] access for the
20//!     runtime validator.
21//! - Typed helper functions for extension extraction with concrete error types:
22//!   [`field_constraints_typed`], [`message_constraints_typed`],
23//!   [`oneof_constraints_typed`], [`predefined_constraints_typed`].
24//!
25//! # Usage
26//!
27//! Most users do not need this crate directly — the
28//! [`prost-protovalidate`](https://crates.io/crates/prost-protovalidate) crate re-exports
29//! everything required for validation via its `types` module. Use this crate when you only need the
30//! generated types or descriptor pool without the evaluation engine.
31
32#![warn(missing_docs)]
33
34#[allow(
35    missing_docs,
36    clippy::len_without_is_empty,
37    clippy::doc_lazy_continuation,
38    clippy::doc_markdown,
39    clippy::must_use_candidate
40)]
41mod proto;
42
43pub mod rules_meta;
44
45use std::sync::LazyLock;
46
47use prost::Message;
48use prost_reflect::{
49    DynamicMessage, ExtensionDescriptor, FieldDescriptor, MessageDescriptor, OneofDescriptor,
50};
51
52pub use proto::*;
53
54// buf.validate extensions use field number 1159
55static BUF_VALIDATE_MESSAGE: LazyLock<Option<ExtensionDescriptor>> =
56    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.message"));
57
58static BUF_VALIDATE_ONEOF: LazyLock<Option<ExtensionDescriptor>> =
59    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.oneof"));
60
61static BUF_VALIDATE_FIELD: LazyLock<Option<ExtensionDescriptor>> =
62    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.field"));
63
64static BUF_VALIDATE_PREDEFINED: LazyLock<Option<ExtensionDescriptor>> =
65    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.predefined"));
66
67/// Error returned while decoding `buf.validate` descriptor extensions.
68#[derive(Debug, thiserror::Error)]
69pub enum ConstraintDecodeError {
70    /// The generated descriptor pool could not be decoded.
71    #[error("descriptor pool initialization failed: {0}")]
72    DescriptorPoolInitialization(String),
73
74    /// The expected extension descriptor is missing from the pool.
75    #[error("missing extension descriptor `{0}`")]
76    MissingExtension(&'static str),
77
78    /// The extension payload could not be decoded into the typed rule.
79    #[error(transparent)]
80    Decode(#[from] prost::DecodeError),
81}
82
83/// Typed decode result for descriptor extension constraints.
84pub type ConstraintDecodeResult<T> = Result<Option<T>, ConstraintDecodeError>;
85
86fn decode_extension_constraints<T>(
87    options: &DynamicMessage,
88    extension_name: &'static str,
89    extension: &'static LazyLock<Option<ExtensionDescriptor>>,
90) -> ConstraintDecodeResult<T>
91where
92    T: Message + Default,
93{
94    let extension = resolve_extension_descriptor(extension_name, extension)?;
95    if !options.has_extension(extension) {
96        return Ok(None);
97    }
98    match options.get_extension(extension).as_message() {
99        Some(message) => message.transcode_to::<T>().map(Some).map_err(Into::into),
100        None => Ok(None),
101    }
102}
103
104fn resolve_extension_descriptor(
105    extension_name: &'static str,
106    extension: &'static LazyLock<Option<ExtensionDescriptor>>,
107) -> Result<&'static ExtensionDescriptor, ConstraintDecodeError> {
108    if let Some(err) = descriptor_pool_decode_error() {
109        return Err(ConstraintDecodeError::DescriptorPoolInitialization(
110            err.to_string(),
111        ));
112    }
113
114    extension
115        .as_ref()
116        .ok_or(ConstraintDecodeError::MissingExtension(extension_name))
117}
118
119/// Typed helper for extracting `buf.validate.field` rules from a field descriptor.
120///
121/// Prefer this API when you need a concrete, non-erased error type.
122///
123/// # Errors
124///
125/// Returns an error if the extension value cannot be transcoded to `FieldRules`.
126pub fn field_constraints_typed(field: &FieldDescriptor) -> ConstraintDecodeResult<FieldRules> {
127    let options = field.options();
128    decode_extension_constraints(&options, "buf.validate.field", &BUF_VALIDATE_FIELD)
129}
130
131/// Typed helper for extracting `buf.validate.oneof` rules from a oneof descriptor.
132///
133/// # Errors
134///
135/// Returns an error if the extension value cannot be transcoded to `OneofRules`.
136pub fn oneof_constraints_typed(oneof: &OneofDescriptor) -> ConstraintDecodeResult<OneofRules> {
137    let options = oneof.options();
138    decode_extension_constraints(&options, "buf.validate.oneof", &BUF_VALIDATE_ONEOF)
139}
140
141/// Typed helper for extracting `buf.validate.message` rules from a message descriptor.
142///
143/// Prefer this API when you need a concrete, non-erased error type.
144///
145/// # Errors
146///
147/// Returns an error if the extension value cannot be transcoded to `MessageRules`.
148pub fn message_constraints_typed(
149    message: &MessageDescriptor,
150) -> ConstraintDecodeResult<MessageRules> {
151    let options = message.options();
152    decode_extension_constraints(&options, "buf.validate.message", &BUF_VALIDATE_MESSAGE)
153}
154
155/// Typed helper for extracting `buf.validate.predefined` rules from a field descriptor.
156///
157/// Prefer this API when you need a concrete, non-erased error type.
158///
159/// # Errors
160///
161/// Returns an error if the extension value cannot be transcoded to `PredefinedRules`.
162pub fn predefined_constraints_typed(
163    field: &FieldDescriptor,
164) -> ConstraintDecodeResult<PredefinedRules> {
165    let options = field.options();
166    decode_extension_constraints(
167        &options,
168        "buf.validate.predefined",
169        &BUF_VALIDATE_PREDEFINED,
170    )
171}
172
173/// Extension trait for extracting `buf.validate.field` rules from a field descriptor.
174pub trait FieldConstraintsExt {
175    /// Returns the `FieldRules` for this field, if any.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if the extension value cannot be transcoded to `FieldRules`.
180    fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules>;
181
182    /// Returns the real (non-synthetic) oneof containing this field, if any.
183    fn real_oneof(&self) -> Option<OneofDescriptor>;
184
185    /// Returns true if this field is proto3 optional (synthetic oneof).
186    fn is_optional(&self) -> bool;
187}
188
189impl FieldConstraintsExt for FieldDescriptor {
190    fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules> {
191        field_constraints_typed(self)
192    }
193
194    fn real_oneof(&self) -> Option<OneofDescriptor> {
195        self.containing_oneof().filter(|o| !o.is_synthetic())
196    }
197
198    fn is_optional(&self) -> bool {
199        self.containing_oneof().is_some_and(|d| d.is_synthetic())
200    }
201}
202
203/// Extension trait for extracting `buf.validate.oneof` rules from a oneof descriptor.
204pub trait OneofConstraintsExt {
205    /// Returns the `OneofRules` for this oneof, if any.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if the extension value cannot be transcoded to `OneofRules`.
210    fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules>;
211
212    /// Returns true if this oneof requires exactly one field to be set.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the extension value cannot be transcoded to `OneofRules`.
217    fn try_is_required(&self) -> Result<bool, ConstraintDecodeError> {
218        Ok(self
219            .oneof_constraints()?
220            .is_some_and(|rules| rules.required.unwrap_or(false)))
221    }
222}
223
224impl OneofConstraintsExt for OneofDescriptor {
225    fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules> {
226        oneof_constraints_typed(self)
227    }
228}
229
230/// Extension trait for extracting `buf.validate.message` rules from a message descriptor.
231pub trait MessageConstraintsExt {
232    /// Returns the `MessageRules` for this message, if any.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if the extension value cannot be transcoded to `MessageRules`.
237    fn message_constraints(&self) -> ConstraintDecodeResult<MessageRules>;
238}
239
240impl MessageConstraintsExt for MessageDescriptor {
241    fn message_constraints(&self) -> ConstraintDecodeResult<MessageRules> {
242        message_constraints_typed(self)
243    }
244}
245
246/// Extension trait for extracting `buf.validate.predefined` rules from a field descriptor.
247pub trait PredefinedConstraintsExt {
248    /// Returns the `PredefinedRules` for this field, if any.
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if the extension value cannot be transcoded to `PredefinedRules`.
253    fn predefined_constraints(&self) -> ConstraintDecodeResult<PredefinedRules>;
254}
255
256impl PredefinedConstraintsExt for FieldDescriptor {
257    fn predefined_constraints(&self) -> ConstraintDecodeResult<PredefinedRules> {
258        predefined_constraints_typed(self)
259    }
260}
261
262/// Extension trait for extracting the `DynamicMessage` form of field constraints.
263/// This is useful for the runtime validator which needs to read rule fields dynamically.
264pub trait FieldConstraintsDynExt {
265    /// Returns the raw `DynamicMessage` for the `buf.validate.field` extension.
266    fn field_constraints_dynamic(&self) -> Option<DynamicMessage>;
267}
268
269impl FieldConstraintsDynExt for FieldDescriptor {
270    fn field_constraints_dynamic(&self) -> Option<DynamicMessage> {
271        let options = self.options();
272        let Ok(extension) = resolve_extension_descriptor("buf.validate.field", &BUF_VALIDATE_FIELD)
273        else {
274            return None;
275        };
276        if !options.has_extension(extension) {
277            return None;
278        }
279        options.get_extension(extension).as_message().cloned()
280    }
281}
282
283/// Extension trait for extracting the `DynamicMessage` form of message constraints.
284pub trait MessageConstraintsDynExt {
285    /// Returns the raw `DynamicMessage` for the `buf.validate.message` extension.
286    fn message_constraints_dynamic(&self) -> Option<DynamicMessage>;
287}
288
289impl MessageConstraintsDynExt for MessageDescriptor {
290    fn message_constraints_dynamic(&self) -> Option<DynamicMessage> {
291        let options = self.options();
292        let Ok(extension) =
293            resolve_extension_descriptor("buf.validate.message", &BUF_VALIDATE_MESSAGE)
294        else {
295            return None;
296        };
297        if !options.has_extension(extension) {
298            return None;
299        }
300        options.get_extension(extension).as_message().cloned()
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use pretty_assertions::assert_eq;
307
308    use super::*;
309
310    fn descriptor_field(message: &str, field: &str) -> FieldDescriptor {
311        DESCRIPTOR_POOL
312            .get_message_by_name(message)
313            .and_then(|message| message.get_field_by_name(field))
314            .expect("descriptor field must exist")
315    }
316
317    #[test]
318    fn typed_helpers_return_none_when_extension_is_absent() {
319        let field = descriptor_field("buf.validate.FieldRules", "required");
320        let message = DESCRIPTOR_POOL
321            .get_message_by_name("buf.validate.FieldRules")
322            .expect("message must exist");
323        let oneof = message
324            .oneofs()
325            .find(|oneof| oneof.name() == "type")
326            .expect("oneof must exist");
327
328        assert_eq!(field_constraints_typed(&field).ok().flatten(), None);
329        assert_eq!(message_constraints_typed(&message).ok().flatten(), None);
330        assert_eq!(oneof_constraints_typed(&oneof).ok().flatten(), None);
331    }
332
333    #[test]
334    fn typed_predefined_helper_decodes_known_extension() {
335        let field = descriptor_field("buf.validate.RepeatedRules", "min_items");
336        let rules = predefined_constraints_typed(&field)
337            .expect("predefined extension should decode")
338            .expect("predefined extension should be present");
339        assert!(!rules.cel.is_empty());
340    }
341}