prost_protovalidate_types/
lib.rs1#![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
54static 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#[derive(Debug, thiserror::Error)]
69pub enum ConstraintDecodeError {
70 #[error("descriptor pool initialization failed: {0}")]
72 DescriptorPoolInitialization(String),
73
74 #[error("missing extension descriptor `{0}`")]
76 MissingExtension(&'static str),
77
78 #[error(transparent)]
80 Decode(#[from] prost::DecodeError),
81}
82
83pub 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
119pub 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
131pub 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
141pub 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
155pub 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
173pub trait FieldConstraintsExt {
175 fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules>;
181
182 fn real_oneof(&self) -> Option<OneofDescriptor>;
184
185 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
203pub trait OneofConstraintsExt {
205 fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules>;
211
212 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
230pub trait MessageConstraintsExt {
232 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
246pub trait PredefinedConstraintsExt {
248 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
262pub trait FieldConstraintsDynExt {
265 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
283pub trait MessageConstraintsDynExt {
285 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}