Skip to main content

prost_protovalidate/
tonic.rs

1//! Optional integration with [`tonic`] (gRPC).
2//!
3//! Enabled by the `tonic` cargo feature. Provides a typed bridge between
4//! [`ValidationError`] / [`Error`] and [`tonic::Status`], plus a
5//! [`ValidateRequest`] extension trait for one-line per-handler validation:
6//!
7//! ```ignore
8//! use prost_protovalidate::{Validate, tonic::ValidateRequest};
9//!
10//! #[tonic::async_trait]
11//! impl my_proto::greeter_server::Greeter for GreeterImpl {
12//!     async fn say_hello(
13//!         &self,
14//!         req: tonic::Request<my_proto::HelloRequest>,
15//!     ) -> Result<tonic::Response<my_proto::HelloReply>, tonic::Status> {
16//!         req.validate_inner()?;
17//!         // ... handler body ...
18//! #       unimplemented!()
19//!     }
20//! }
21//! ```
22//!
23//! With the additional `tonic-types` feature on, the resulting
24//! [`tonic::Status`] carries a `google.rpc.BadRequest` detail with one
25//! `FieldViolation` per [`Violation`](crate::Violation), so clients can
26//! parse field-level errors without scraping the message string.
27//!
28//! # Mapping to gRPC codes
29//!
30//! - [`ValidationError`] → [`tonic::Code::InvalidArgument`]. The status
31//!   message is the [`Display`](std::fmt::Display) form of the error
32//!   (a list of `{field}: {message}` lines), which is safe to expose to
33//!   clients since it derives from rules the client violated.
34//! - [`CompilationError`] / [`RuntimeError`] → [`tonic::Code::Internal`]
35//!   with a **fixed, generic message**. The underlying `cause` strings can
36//!   contain proto field names, CEL parse output, or type-mismatch details
37//!   that should not be exposed to untrusted clients. Callers who need the
38//!   full cause for server-side logging must inspect/log the error
39//!   **before** invoking the `Into` conversion.
40//!
41//! # Streaming requests
42//!
43//! [`ValidateRequest`] applies to handlers whose request shape is
44//! `tonic::Request<T>` (unary and server-streaming). For client-streaming
45//! and bidirectional handlers, where the request is
46//! `tonic::Request<tonic::Streaming<T>>`, validate each message inside the
47//! per-message loop:
48//!
49//! ```ignore
50//! use prost_protovalidate::Validate;
51//! while let Some(msg) = stream.message().await? {
52//!     msg.validate().map_err(tonic::Status::from)?;
53//!     // ... process msg ...
54//! }
55//! ```
56
57use crate::Validate;
58use crate::error::{CompilationError, Error, RuntimeError, ValidationError};
59
60impl From<ValidationError> for tonic::Status {
61    fn from(err: ValidationError) -> Self {
62        #[cfg(feature = "tonic-types")]
63        {
64            use tonic_types::{ErrorDetails, StatusExt};
65            let mut error_details = ErrorDetails::new();
66            for v in err.violations() {
67                let description = if !v.message().is_empty() {
68                    v.message().to_string()
69                } else if !v.rule_id().is_empty() {
70                    format!("[{}]", v.rule_id())
71                } else {
72                    "[unknown]".to_string()
73                };
74                error_details.add_bad_request_violation(v.field_path(), description);
75            }
76            tonic::Status::with_error_details(
77                tonic::Code::InvalidArgument,
78                err.to_string(),
79                error_details,
80            )
81        }
82        #[cfg(not(feature = "tonic-types"))]
83        {
84            tonic::Status::invalid_argument(err.to_string())
85        }
86    }
87}
88
89impl From<CompilationError> for tonic::Status {
90    /// Maps to [`tonic::Code::Internal`] with a fixed, generic message.
91    ///
92    /// The original `cause` is **not** forwarded — it can contain proto field
93    /// names or CEL internals that should not be exposed to untrusted clients.
94    /// Log the underlying error server-side before converting.
95    fn from(_err: CompilationError) -> Self {
96        tonic::Status::internal("validation rule compilation failed")
97    }
98}
99
100impl From<RuntimeError> for tonic::Status {
101    /// Maps to [`tonic::Code::Internal`] with a fixed, generic message.
102    ///
103    /// The original `cause` is **not** forwarded — it can contain proto field
104    /// names, type-mismatch details, or CEL evaluation internals that should
105    /// not be exposed to untrusted clients. Log the underlying error
106    /// server-side before converting.
107    fn from(_err: RuntimeError) -> Self {
108        tonic::Status::internal("validation rule evaluation failed")
109    }
110}
111
112impl From<Error> for tonic::Status {
113    fn from(err: Error) -> Self {
114        match err {
115            Error::Validation(e) => e.into(),
116            Error::Compilation(e) => e.into(),
117            Error::Runtime(e) => e.into(),
118        }
119    }
120}
121
122/// Extension trait that calls [`Validate::validate`] on the inner message of
123/// a [`tonic::Request`] and maps any [`ValidationError`] to a
124/// [`tonic::Status`] with `Code::InvalidArgument`.
125///
126/// Applies to handlers whose request shape is `tonic::Request<T>` — unary
127/// and server-streaming RPCs. For client-streaming and bidirectional RPCs
128/// where the request is `tonic::Request<tonic::Streaming<T>>`, see the
129/// per-message pattern in the module-level docs.
130pub trait ValidateRequest {
131    /// Validate the inner message of this gRPC request.
132    ///
133    /// # Errors
134    /// Returns a [`tonic::Status`] with `Code::InvalidArgument` if any
135    /// validation rule failed. With the `tonic-types` feature on, the
136    /// status carries a `google.rpc.BadRequest` detail with one
137    /// `FieldViolation` per [`Violation`](crate::Violation).
138    #[must_use = "discarding a validation result allows invalid requests through"]
139    fn validate_inner(&self) -> Result<(), tonic::Status>;
140}
141
142impl<T: Validate> ValidateRequest for tonic::Request<T> {
143    fn validate_inner(&self) -> Result<(), tonic::Status> {
144        self.get_ref().validate().map_err(Into::into)
145    }
146}