Skip to main content

prost_protovalidate_types/
constraints.rs

1//! Descriptor-based constraint extraction (requires the `reflect` feature).
2//!
3//! Extension-descriptor statics plus the typed helpers and extension traits
4//! for pulling `buf.validate` rules out of `prost-reflect` descriptors.
5
6use std::sync::LazyLock;
7
8use prost::Message;
9use prost_reflect::{
10    DynamicMessage, ExtensionDescriptor, FieldDescriptor, MessageDescriptor, OneofDescriptor,
11};
12
13use crate::proto::{DESCRIPTOR_POOL, descriptor_pool_decode_error};
14use crate::{
15    ConstraintDecodeError, ConstraintDecodeResult, FieldRules, MessageRules, OneofRules,
16    PredefinedRules,
17};
18
19// buf.validate extensions use field number 1159
20static BUF_VALIDATE_MESSAGE: LazyLock<Option<ExtensionDescriptor>> =
21    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.message"));
22
23static BUF_VALIDATE_ONEOF: LazyLock<Option<ExtensionDescriptor>> =
24    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.oneof"));
25
26static BUF_VALIDATE_FIELD: LazyLock<Option<ExtensionDescriptor>> =
27    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.field"));
28
29static BUF_VALIDATE_PREDEFINED: LazyLock<Option<ExtensionDescriptor>> =
30    LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.predefined"));
31
32fn decode_extension_constraints<T>(
33    options: &DynamicMessage,
34    extension_name: &'static str,
35    extension: &'static LazyLock<Option<ExtensionDescriptor>>,
36) -> ConstraintDecodeResult<T>
37where
38    T: Message + Default,
39{
40    let extension = resolve_extension_descriptor(extension_name, extension)?;
41    if !options.has_extension(extension) {
42        return Ok(None);
43    }
44    match options.get_extension(extension).as_message() {
45        Some(message) => message.transcode_to::<T>().map(Some).map_err(Into::into),
46        None => Ok(None),
47    }
48}
49
50fn resolve_extension_descriptor(
51    extension_name: &'static str,
52    extension: &'static LazyLock<Option<ExtensionDescriptor>>,
53) -> Result<&'static ExtensionDescriptor, ConstraintDecodeError> {
54    if let Some(err) = descriptor_pool_decode_error() {
55        return Err(ConstraintDecodeError::DescriptorPoolInitialization(
56            err.to_string(),
57        ));
58    }
59
60    extension
61        .as_ref()
62        .ok_or(ConstraintDecodeError::MissingExtension(extension_name))
63}
64
65/// Typed helper for extracting `buf.validate.field` rules from a field descriptor.
66///
67/// Prefer this API when you need a concrete, non-erased error type.
68///
69/// # Errors
70///
71/// Returns an error if the extension value cannot be transcoded to `FieldRules`.
72pub fn field_constraints_typed(field: &FieldDescriptor) -> ConstraintDecodeResult<FieldRules> {
73    let options = field.options();
74    decode_extension_constraints(&options, "buf.validate.field", &BUF_VALIDATE_FIELD)
75}
76
77/// Typed helper for extracting `buf.validate.oneof` rules from a oneof descriptor.
78///
79/// # Errors
80///
81/// Returns an error if the extension value cannot be transcoded to `OneofRules`.
82pub fn oneof_constraints_typed(oneof: &OneofDescriptor) -> ConstraintDecodeResult<OneofRules> {
83    let options = oneof.options();
84    decode_extension_constraints(&options, "buf.validate.oneof", &BUF_VALIDATE_ONEOF)
85}
86
87/// Typed helper for extracting `buf.validate.message` rules from a message descriptor.
88///
89/// Prefer this API when you need a concrete, non-erased error type.
90///
91/// # Errors
92///
93/// Returns an error if the extension value cannot be transcoded to `MessageRules`.
94pub fn message_constraints_typed(
95    message: &MessageDescriptor,
96) -> ConstraintDecodeResult<MessageRules> {
97    let options = message.options();
98    decode_extension_constraints(&options, "buf.validate.message", &BUF_VALIDATE_MESSAGE)
99}
100
101/// Typed helper for extracting `buf.validate.predefined` rules from a field descriptor.
102///
103/// Prefer this API when you need a concrete, non-erased error type.
104///
105/// # Errors
106///
107/// Returns an error if the extension value cannot be transcoded to `PredefinedRules`.
108pub fn predefined_constraints_typed(
109    field: &FieldDescriptor,
110) -> ConstraintDecodeResult<PredefinedRules> {
111    let options = field.options();
112    decode_extension_constraints(
113        &options,
114        "buf.validate.predefined",
115        &BUF_VALIDATE_PREDEFINED,
116    )
117}
118
119/// Extension trait for extracting `buf.validate.field` rules from a field descriptor.
120pub trait FieldConstraintsExt {
121    /// Returns the `FieldRules` for this field, if any.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the extension value cannot be transcoded to `FieldRules`.
126    fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules>;
127
128    /// Returns the real (non-synthetic) oneof containing this field, if any.
129    fn real_oneof(&self) -> Option<OneofDescriptor>;
130
131    /// Returns true if this field is proto3 optional (synthetic oneof).
132    fn is_optional(&self) -> bool;
133}
134
135impl FieldConstraintsExt for FieldDescriptor {
136    fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules> {
137        field_constraints_typed(self)
138    }
139
140    fn real_oneof(&self) -> Option<OneofDescriptor> {
141        self.containing_oneof().filter(|o| !o.is_synthetic())
142    }
143
144    fn is_optional(&self) -> bool {
145        self.containing_oneof().is_some_and(|d| d.is_synthetic())
146    }
147}
148
149/// Extension trait for extracting `buf.validate.oneof` rules from a oneof descriptor.
150pub trait OneofConstraintsExt {
151    /// Returns the `OneofRules` for this oneof, if any.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if the extension value cannot be transcoded to `OneofRules`.
156    fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules>;
157
158    /// Returns true if this oneof requires exactly one field to be set.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the extension value cannot be transcoded to `OneofRules`.
163    fn try_is_required(&self) -> Result<bool, ConstraintDecodeError> {
164        Ok(self
165            .oneof_constraints()?
166            .is_some_and(|rules| rules.required.unwrap_or(false)))
167    }
168}
169
170impl OneofConstraintsExt for OneofDescriptor {
171    fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules> {
172        oneof_constraints_typed(self)
173    }
174}
175
176/// Extension trait for extracting `buf.validate.message` rules from a message descriptor.
177pub trait MessageConstraintsExt {
178    /// Returns the `MessageRules` for this message, if any.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the extension value cannot be transcoded to `MessageRules`.
183    fn message_constraints(&self) -> ConstraintDecodeResult<MessageRules>;
184}
185
186impl MessageConstraintsExt for MessageDescriptor {
187    fn message_constraints(&self) -> ConstraintDecodeResult<MessageRules> {
188        message_constraints_typed(self)
189    }
190}
191
192/// Extension trait for extracting `buf.validate.predefined` rules from a field descriptor.
193pub trait PredefinedConstraintsExt {
194    /// Returns the `PredefinedRules` for this field, if any.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the extension value cannot be transcoded to `PredefinedRules`.
199    fn predefined_constraints(&self) -> ConstraintDecodeResult<PredefinedRules>;
200}
201
202impl PredefinedConstraintsExt for FieldDescriptor {
203    fn predefined_constraints(&self) -> ConstraintDecodeResult<PredefinedRules> {
204        predefined_constraints_typed(self)
205    }
206}
207
208/// Extension trait for extracting the `DynamicMessage` form of field constraints.
209/// This is useful for the runtime validator which needs to read rule fields dynamically.
210pub trait FieldConstraintsDynExt {
211    /// Returns the raw `DynamicMessage` for the `buf.validate.field` extension.
212    fn field_constraints_dynamic(&self) -> Option<DynamicMessage>;
213}
214
215impl FieldConstraintsDynExt for FieldDescriptor {
216    fn field_constraints_dynamic(&self) -> Option<DynamicMessage> {
217        let options = self.options();
218        let Ok(extension) = resolve_extension_descriptor("buf.validate.field", &BUF_VALIDATE_FIELD)
219        else {
220            return None;
221        };
222        if !options.has_extension(extension) {
223            return None;
224        }
225        options.get_extension(extension).as_message().cloned()
226    }
227}
228
229/// Extension trait for extracting the `DynamicMessage` form of message constraints.
230pub trait MessageConstraintsDynExt {
231    /// Returns the raw `DynamicMessage` for the `buf.validate.message` extension.
232    fn message_constraints_dynamic(&self) -> Option<DynamicMessage>;
233}
234
235impl MessageConstraintsDynExt for MessageDescriptor {
236    fn message_constraints_dynamic(&self) -> Option<DynamicMessage> {
237        let options = self.options();
238        let Ok(extension) =
239            resolve_extension_descriptor("buf.validate.message", &BUF_VALIDATE_MESSAGE)
240        else {
241            return None;
242        };
243        if !options.has_extension(extension) {
244            return None;
245        }
246        options.get_extension(extension).as_message().cloned()
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use pretty_assertions::assert_eq;
253
254    use super::*;
255
256    fn descriptor_field(message: &str, field: &str) -> FieldDescriptor {
257        DESCRIPTOR_POOL
258            .get_message_by_name(message)
259            .and_then(|message| message.get_field_by_name(field))
260            .expect("descriptor field must exist")
261    }
262
263    #[test]
264    fn typed_helpers_return_none_when_extension_is_absent() {
265        let field = descriptor_field("buf.validate.FieldRules", "required");
266        let message = DESCRIPTOR_POOL
267            .get_message_by_name("buf.validate.FieldRules")
268            .expect("message must exist");
269        let oneof = message
270            .oneofs()
271            .find(|oneof| oneof.name() == "type")
272            .expect("oneof must exist");
273
274        assert_eq!(field_constraints_typed(&field).ok().flatten(), None);
275        assert_eq!(message_constraints_typed(&message).ok().flatten(), None);
276        assert_eq!(oneof_constraints_typed(&oneof).ok().flatten(), None);
277    }
278
279    #[test]
280    fn typed_predefined_helper_decodes_known_extension() {
281        let field = descriptor_field("buf.validate.RepeatedRules", "min_items");
282        let rules = predefined_constraints_typed(&field)
283            .expect("predefined extension should decode")
284            .expect("predefined extension should be present");
285        assert!(!rules.cel.is_empty());
286    }
287}