prost_protovalidate/bridge.rs
1//! Runtime bridge for validating wire-encoded messages through the
2//! descriptor-driven [`Validator`].
3//!
4//! Used by `prost-protovalidate-build`'s `runtime_bridge` codegen mode (buffa
5//! backend): messages whose rules the compile-time generator cannot cover
6//! (CEL, predefined CEL, or shapes routed to the runtime) get an
7//! `impl Validate` that encodes the message to protobuf wire bytes and
8//! delegates here, reusing the full runtime engine — including the CEL
9//! interpreter — instead of a second validation code path. Standard-only
10//! messages keep their inline generated validators; only routed messages
11//! reach this bridge.
12//!
13//! Reflection-gated: available only with the `reflect` feature (implied by
14//! `cel`). CEL rules additionally require `cel`.
15//!
16//! The generated code references only this module's public surface
17//! (`::prost_protovalidate::bridge::*`), so a consumer needs no direct
18//! dependency on `prost-reflect`.
19
20use prost_reflect::{DescriptorPool, DynamicMessage};
21
22use crate::{
23 Error, RuntimeError, ValidationError, Validator, ValidatorOption, Violation,
24 normalize_edition_descriptor_set,
25};
26
27/// A descriptor pool plus a [`Validator`], built once from an embedded
28/// `FileDescriptorSet` and reused across validations.
29///
30/// Construct once — typically in a `LazyLock` static emitted by the code
31/// generator — and reuse: the inner [`Validator`] caches compiled evaluators
32/// across calls.
33pub struct RuntimeBridge {
34 pool: DescriptorPool,
35 validator: Validator,
36}
37
38impl RuntimeBridge {
39 /// Build a bridge from raw `FileDescriptorSet` bytes.
40 ///
41 /// The bytes are edition-normalized (Edition 2023 → proto3, matching the
42 /// runtime validator's decode pool) before the pool and validator are
43 /// constructed — mirroring the conformance executor's suite setup. The
44 /// same normalized bytes back both the decode pool and the validator's
45 /// extension resolution.
46 ///
47 /// # Panics
48 ///
49 /// Panics if the descriptor set cannot be decoded. The bytes are embedded
50 /// at build time by `prost-protovalidate-build`, so a failure indicates a
51 /// generator bug rather than runtime input.
52 #[must_use]
53 pub fn from_fds(fds: &[u8]) -> Self {
54 let normalized = normalize_edition_descriptor_set(fds);
55 let pool = DescriptorPool::decode(normalized.as_slice())
56 .expect("prost-protovalidate bridge: embedded descriptor set must decode");
57 let validator =
58 Validator::with_options(&[ValidatorOption::AdditionalDescriptorSetBytes(normalized)]);
59 Self { pool, validator }
60 }
61
62 /// Validate a wire-encoded message of type `full_name` against its
63 /// `buf.validate` rules through the runtime [`Validator`].
64 ///
65 /// `wire_bytes` is the protobuf binary encoding of the message (e.g. from
66 /// `buffa::Message::encode_to_vec`). The full [`Error`] is returned so
67 /// callers that must distinguish `Compilation`/`Runtime` outcomes
68 /// (conformance, diagnostics) can; the generated `Validate` impls collapse
69 /// it to [`ValidationError`] via [`error_to_validation_error`].
70 ///
71 /// # Errors
72 ///
73 /// Returns [`Error::Validation`] for rule violations, or
74 /// [`Error::Compilation`] / [`Error::Runtime`] when rule evaluation fails.
75 /// Returns [`Error::Runtime`] if `full_name` is absent from the pool or the
76 /// bytes fail to decode.
77 pub fn validate_wire(&self, full_name: &str, wire_bytes: &[u8]) -> Result<(), Error> {
78 let descriptor = self.pool.get_message_by_name(full_name).ok_or_else(|| {
79 Error::Runtime(RuntimeError {
80 cause: format!("bridge: message type `{full_name}` not found in descriptor pool"),
81 })
82 })?;
83 let dynamic = DynamicMessage::decode(descriptor, wire_bytes).map_err(|e| {
84 Error::Runtime(RuntimeError {
85 cause: format!("bridge: failed to decode `{full_name}`: {e}"),
86 })
87 })?;
88 self.validator.validate(&dynamic)
89 }
90}
91
92/// Collapse a runtime [`Error`] into the [`ValidationError`] the
93/// [`Validate`](crate::Validate) trait returns.
94///
95/// [`Error::Validation`] passes through unchanged. [`Error::Compilation`] and
96/// [`Error::Runtime`] cannot be represented by the trait's return type, so they
97/// surface as a single synthesized [`Violation`] carrying the error's cause
98/// (empty `rule_id`) rather than silently succeeding — the same
99/// known-limitation class as `bytes.pattern` on invalid UTF-8 in the
100/// compile-time path. Callers needing to distinguish these should use
101/// [`RuntimeBridge::validate_wire`] directly.
102#[must_use]
103pub fn error_to_validation_error(error: Error) -> ValidationError {
104 match error {
105 Error::Validation(err) => err,
106 Error::Compilation(err) => ValidationError::single(Violation::new("", "", err.cause)),
107 Error::Runtime(err) => ValidationError::single(Violation::new("", "", err.cause)),
108 }
109}