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
43use std::sync::LazyLock;
44
45use prost::Message;
46use prost_reflect::{
47 DynamicMessage, ExtensionDescriptor, FieldDescriptor, MessageDescriptor, OneofDescriptor,
48};
49
50pub use proto::*;
51
52static BUF_VALIDATE_MESSAGE: LazyLock<Option<ExtensionDescriptor>> =
54 LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.message"));
55
56static BUF_VALIDATE_ONEOF: LazyLock<Option<ExtensionDescriptor>> =
57 LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.oneof"));
58
59static BUF_VALIDATE_FIELD: LazyLock<Option<ExtensionDescriptor>> =
60 LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.field"));
61
62static BUF_VALIDATE_PREDEFINED: LazyLock<Option<ExtensionDescriptor>> =
63 LazyLock::new(|| DESCRIPTOR_POOL.get_extension_by_name("buf.validate.predefined"));
64
65#[derive(Debug, thiserror::Error)]
67pub enum ConstraintDecodeError {
68 #[error("descriptor pool initialization failed: {0}")]
70 DescriptorPoolInitialization(String),
71
72 #[error("missing extension descriptor `{0}`")]
74 MissingExtension(&'static str),
75
76 #[error(transparent)]
78 Decode(#[from] prost::DecodeError),
79}
80
81pub type ConstraintDecodeResult<T> = Result<Option<T>, ConstraintDecodeError>;
83
84fn decode_extension_constraints<T>(
85 options: &DynamicMessage,
86 extension_name: &'static str,
87 extension: &'static LazyLock<Option<ExtensionDescriptor>>,
88) -> ConstraintDecodeResult<T>
89where
90 T: Message + Default,
91{
92 let extension = resolve_extension_descriptor(extension_name, extension)?;
93 if !options.has_extension(extension) {
94 return Ok(None);
95 }
96 match options.get_extension(extension).as_message() {
97 Some(message) => message.transcode_to::<T>().map(Some).map_err(Into::into),
98 None => Ok(None),
99 }
100}
101
102fn resolve_extension_descriptor(
103 extension_name: &'static str,
104 extension: &'static LazyLock<Option<ExtensionDescriptor>>,
105) -> Result<&'static ExtensionDescriptor, ConstraintDecodeError> {
106 if let Some(err) = descriptor_pool_decode_error() {
107 return Err(ConstraintDecodeError::DescriptorPoolInitialization(
108 err.to_string(),
109 ));
110 }
111
112 extension
113 .as_ref()
114 .ok_or(ConstraintDecodeError::MissingExtension(extension_name))
115}
116
117pub fn field_constraints_typed(field: &FieldDescriptor) -> ConstraintDecodeResult<FieldRules> {
125 let options = field.options();
126 decode_extension_constraints(&options, "buf.validate.field", &BUF_VALIDATE_FIELD)
127}
128
129pub fn oneof_constraints_typed(oneof: &OneofDescriptor) -> ConstraintDecodeResult<OneofRules> {
135 let options = oneof.options();
136 decode_extension_constraints(&options, "buf.validate.oneof", &BUF_VALIDATE_ONEOF)
137}
138
139pub fn message_constraints_typed(
147 message: &MessageDescriptor,
148) -> ConstraintDecodeResult<MessageRules> {
149 let options = message.options();
150 decode_extension_constraints(&options, "buf.validate.message", &BUF_VALIDATE_MESSAGE)
151}
152
153pub fn predefined_constraints_typed(
161 field: &FieldDescriptor,
162) -> ConstraintDecodeResult<PredefinedRules> {
163 let options = field.options();
164 decode_extension_constraints(
165 &options,
166 "buf.validate.predefined",
167 &BUF_VALIDATE_PREDEFINED,
168 )
169}
170
171pub trait FieldConstraintsExt {
173 fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules>;
179
180 fn real_oneof(&self) -> Option<OneofDescriptor>;
182
183 fn is_optional(&self) -> bool;
185}
186
187impl FieldConstraintsExt for FieldDescriptor {
188 fn field_constraints(&self) -> ConstraintDecodeResult<FieldRules> {
189 field_constraints_typed(self)
190 }
191
192 fn real_oneof(&self) -> Option<OneofDescriptor> {
193 self.containing_oneof().filter(|o| !o.is_synthetic())
194 }
195
196 fn is_optional(&self) -> bool {
197 self.containing_oneof().is_some_and(|d| d.is_synthetic())
198 }
199}
200
201pub trait OneofConstraintsExt {
203 fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules>;
209
210 fn try_is_required(&self) -> Result<bool, ConstraintDecodeError> {
216 Ok(self
217 .oneof_constraints()?
218 .is_some_and(|rules| rules.required.unwrap_or(false)))
219 }
220}
221
222impl OneofConstraintsExt for OneofDescriptor {
223 fn oneof_constraints(&self) -> ConstraintDecodeResult<OneofRules> {
224 oneof_constraints_typed(self)
225 }
226}
227
228pub trait MessageConstraintsExt {
230 fn message_constraints(&self) -> ConstraintDecodeResult<MessageRules>;
236}
237
238impl MessageConstraintsExt for MessageDescriptor {
239 fn message_constraints(&self) -> ConstraintDecodeResult<MessageRules> {
240 message_constraints_typed(self)
241 }
242}
243
244pub trait PredefinedConstraintsExt {
246 fn predefined_constraints(&self) -> ConstraintDecodeResult<PredefinedRules>;
252}
253
254impl PredefinedConstraintsExt for FieldDescriptor {
255 fn predefined_constraints(&self) -> ConstraintDecodeResult<PredefinedRules> {
256 predefined_constraints_typed(self)
257 }
258}
259
260pub trait FieldConstraintsDynExt {
263 fn field_constraints_dynamic(&self) -> Option<DynamicMessage>;
265}
266
267impl FieldConstraintsDynExt for FieldDescriptor {
268 fn field_constraints_dynamic(&self) -> Option<DynamicMessage> {
269 let options = self.options();
270 let Ok(extension) = resolve_extension_descriptor("buf.validate.field", &BUF_VALIDATE_FIELD)
271 else {
272 return None;
273 };
274 if !options.has_extension(extension) {
275 return None;
276 }
277 options.get_extension(extension).as_message().cloned()
278 }
279}
280
281pub trait MessageConstraintsDynExt {
283 fn message_constraints_dynamic(&self) -> Option<DynamicMessage>;
285}
286
287impl MessageConstraintsDynExt for MessageDescriptor {
288 fn message_constraints_dynamic(&self) -> Option<DynamicMessage> {
289 let options = self.options();
290 let Ok(extension) =
291 resolve_extension_descriptor("buf.validate.message", &BUF_VALIDATE_MESSAGE)
292 else {
293 return None;
294 };
295 if !options.has_extension(extension) {
296 return None;
297 }
298 options.get_extension(extension).as_message().cloned()
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use pretty_assertions::assert_eq;
305
306 use super::*;
307
308 fn descriptor_field(message: &str, field: &str) -> FieldDescriptor {
309 DESCRIPTOR_POOL
310 .get_message_by_name(message)
311 .and_then(|message| message.get_field_by_name(field))
312 .expect("descriptor field must exist")
313 }
314
315 #[test]
316 fn typed_helpers_return_none_when_extension_is_absent() {
317 let field = descriptor_field("buf.validate.FieldRules", "required");
318 let message = DESCRIPTOR_POOL
319 .get_message_by_name("buf.validate.FieldRules")
320 .expect("message must exist");
321 let oneof = message
322 .oneofs()
323 .find(|oneof| oneof.name() == "type")
324 .expect("oneof must exist");
325
326 assert_eq!(field_constraints_typed(&field).ok().flatten(), None);
327 assert_eq!(message_constraints_typed(&message).ok().flatten(), None);
328 assert_eq!(oneof_constraints_typed(&oneof).ok().flatten(), None);
329 }
330
331 #[test]
332 fn typed_predefined_helper_decodes_known_extension() {
333 let field = descriptor_field("buf.validate.RepeatedRules", "min_items");
334 let rules = predefined_constraints_typed(&field)
335 .expect("predefined extension should decode")
336 .expect("predefined extension should be present");
337 assert!(!rules.cel.is_empty());
338 }
339}