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}