Skip to main content

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}