vld_tonic/lib.rs
1#![allow(clippy::result_large_err)]
2//! # vld-tonic — tonic gRPC integration for the `vld` validation library
3//!
4//! Validate protobuf messages and gRPC metadata using `vld` schemas.
5//!
6//! ## Approaches
7//!
8//! | Function | Use case |
9//! |---|---|
10//! | [`validate`] / [`validate_ref`] | Validate a message that implements [`VldMessage`] (via [`impl_validate!`]) |
11//! | [`validate_with`] / [`validate_with_ref`] | Validate against a separate `vld::schema!` struct |
12//! | [`validate_metadata`] | Validate gRPC request metadata |
13//! | [`metadata_interceptor`] | Tonic interceptor for automatic metadata validation |
14//!
15//! ## Quick example
16//!
17//! ```ignore
18//! use serde::Serialize;
19//! use tonic::{Request, Response, Status};
20//!
21//! // Protobuf message (generated by prost with serde support)
22//! #[derive(Clone, Serialize)]
23//! pub struct CreateUserRequest {
24//! pub name: String,
25//! pub email: String,
26//! }
27//!
28//! // Attach validation rules
29//! vld_tonic::impl_validate!(CreateUserRequest {
30//! name => vld::string().min(2).max(100),
31//! email => vld::string().email(),
32//! });
33//!
34//! // In your service handler:
35//! async fn create_user(request: Request<CreateUserRequest>) -> Result<Response<()>, Status> {
36//! let msg = vld_tonic::validate(request)?;
37//! // msg is validated
38//! Ok(Response::new(()))
39//! }
40//! ```
41
42use tonic::{Code, Request, Status};
43use vld::error::VldError;
44use vld::schema::VldParse;
45
46// ========================= Error conversion ==================================
47
48/// Convert a [`VldError`] into a [`tonic::Status`] with `INVALID_ARGUMENT` code.
49///
50/// The status message contains a JSON object with structured error details.
51pub fn vld_status(error: &VldError) -> Status {
52 vld_status_with_code(error, Code::InvalidArgument)
53}
54
55/// Convert a [`VldError`] into a [`tonic::Status`] with a custom gRPC code.
56pub fn vld_status_with_code(error: &VldError, code: Code) -> Status {
57 let issues: Vec<serde_json::Value> = error
58 .issues
59 .iter()
60 .map(|issue| {
61 let path: String = issue
62 .path
63 .iter()
64 .map(|p| p.to_string())
65 .collect::<Vec<_>>()
66 .join(".");
67 serde_json::json!({
68 "path": path,
69 "message": issue.message,
70 "code": format!("{:?}", issue.code),
71 })
72 })
73 .collect();
74
75 let body = serde_json::json!({
76 "error": "VALIDATION_ERROR",
77 "message": "Request validation failed",
78 "details": issues,
79 });
80
81 Status::new(code, body.to_string())
82}
83
84// ========================= VldMessage trait ===================================
85
86/// Trait for message types that can be validated with vld.
87///
88/// Implement via the [`impl_validate!`] macro, or manually.
89pub trait VldMessage {
90 /// Validate this message against its rules.
91 fn vld_validate(&self) -> Result<(), VldError>;
92}
93
94// ========================= Message validation ================================
95
96/// Validate a gRPC request message that implements [`VldMessage`].
97///
98/// Consumes the request and returns the validated message,
99/// or `Status::INVALID_ARGUMENT` on failure.
100///
101/// # Example
102///
103/// ```ignore
104/// async fn handler(request: Request<MyMessage>) -> Result<Response<()>, Status> {
105/// let msg = vld_tonic::validate(request)?;
106/// // use msg...
107/// Ok(Response::new(()))
108/// }
109/// ```
110pub fn validate<T: VldMessage>(request: Request<T>) -> Result<T, Status> {
111 let message = request.into_inner();
112 message.vld_validate().map_err(|e| vld_status(&e))?;
113 Ok(message)
114}
115
116/// Validate a message reference without consuming the request.
117pub fn validate_ref<T: VldMessage>(message: &T) -> Result<(), Status> {
118 message.vld_validate().map_err(|e| vld_status(&e))
119}
120
121// ========================= Schema-based validation ===========================
122
123/// Validate a gRPC request message against a separate vld schema type.
124///
125/// The message is serialized to JSON via serde, then validated using
126/// `S::vld_parse_value()`. Useful when you have a `vld::schema!` struct
127/// that mirrors the protobuf message fields.
128///
129/// # Example
130///
131/// ```ignore
132/// vld::schema! {
133/// struct UserSchema {
134/// name: String => vld::string().min(2),
135/// email: String => vld::string().email(),
136/// }
137/// }
138///
139/// async fn handler(request: Request<CreateUserRequest>) -> Result<Response<()>, Status> {
140/// let msg = vld_tonic::validate_with::<UserSchema, _>(request)?;
141/// Ok(Response::new(()))
142/// }
143/// ```
144pub fn validate_with<S, T>(request: Request<T>) -> Result<T, Status>
145where
146 S: VldParse,
147 T: serde::Serialize,
148{
149 let message = request.into_inner();
150 validate_with_ref::<S, T>(&message)?;
151 Ok(message)
152}
153
154/// Validate a message reference against a separate vld schema type.
155pub fn validate_with_ref<S, T>(message: &T) -> Result<(), Status>
156where
157 S: VldParse,
158 T: serde::Serialize,
159{
160 let json = serde_json::to_value(message)
161 .map_err(|e| Status::internal(format!("Serialization error: {}", e)))?;
162 S::vld_parse_value(&json).map_err(|e| vld_status(&e))?;
163 Ok(())
164}
165
166// ========================= Metadata validation ===============================
167
168/// Validate gRPC request metadata against a vld schema.
169///
170/// Metadata keys are converted from `kebab-case` to `snake_case`
171/// (e.g. `x-request-id` → `x_request_id`).
172/// String values are coerced: `"42"` → number, `"true"` → boolean.
173///
174/// # Example
175///
176/// ```ignore
177/// vld::schema! {
178/// #[derive(Debug, Clone)]
179/// struct AuthMeta {
180/// authorization: String => vld::string().min(1),
181/// }
182/// }
183///
184/// async fn handler(request: Request<MyMessage>) -> Result<Response<()>, Status> {
185/// let auth: AuthMeta = vld_tonic::validate_metadata(&request)?;
186/// println!("auth: {}", auth.authorization);
187/// Ok(Response::new(()))
188/// }
189/// ```
190pub fn validate_metadata<T, M>(request: &Request<M>) -> Result<T, Status>
191where
192 T: VldParse,
193{
194 let metadata = request.metadata();
195 let mut map = serde_json::Map::new();
196
197 for kv in metadata.iter() {
198 match kv {
199 tonic::metadata::KeyAndValueRef::Ascii(key, value) => {
200 if let Ok(v) = value.to_str() {
201 let k = key.as_str().replace('-', "_");
202 map.insert(k, coerce_value(v));
203 }
204 }
205 tonic::metadata::KeyAndValueRef::Binary(_, _) => {}
206 }
207 }
208
209 let json = serde_json::Value::Object(map);
210 T::vld_parse_value(&json).map_err(|e| vld_status(&e))
211}
212
213/// Retrieve a validated metadata struct from request extensions.
214///
215/// Use after [`metadata_interceptor`] has stored the validated value.
216///
217/// # Example
218///
219/// ```ignore
220/// async fn handler(request: Request<MyMessage>) -> Result<Response<()>, Status> {
221/// let auth = vld_tonic::validated_metadata::<AuthMeta, _>(&request)
222/// .ok_or_else(|| Status::unauthenticated("Missing auth"))?;
223/// Ok(Response::new(()))
224/// }
225/// ```
226pub fn validated_metadata<T, M>(request: &Request<M>) -> Option<T>
227where
228 T: Clone + Send + Sync + 'static,
229{
230 request.extensions().get::<T>().cloned()
231}
232
233// ========================= Interceptor =======================================
234
235/// A tonic interceptor function that validates request metadata.
236///
237/// The validated struct is stored in request extensions and can be
238/// retrieved in handlers via [`validated_metadata`] or
239/// `request.extensions().get::<T>()`.
240///
241/// # Example
242///
243/// ```ignore
244/// use tonic::transport::Server;
245///
246/// vld::schema! {
247/// #[derive(Debug, Clone)]
248/// struct AuthMeta {
249/// authorization: String => vld::string().min(1),
250/// }
251/// }
252///
253/// Server::builder()
254/// .add_service(MyServiceServer::with_interceptor(
255/// my_service,
256/// vld_tonic::metadata_interceptor::<AuthMeta>,
257/// ))
258/// .serve(addr)
259/// .await?;
260/// ```
261pub fn metadata_interceptor<T>(mut request: Request<()>) -> Result<Request<()>, Status>
262where
263 T: VldParse + Clone + Send + Sync + 'static,
264{
265 let validated: T = validate_metadata(&request)?;
266 request.extensions_mut().insert(validated);
267 Ok(request)
268}
269
270// ========================= Macro =============================================
271
272/// Attach vld validation rules to a protobuf message type.
273///
274/// Generates `validate()` and `is_valid()` inherent methods (via
275/// [`vld::impl_rules!`]) and implements the [`VldMessage`] trait.
276///
277/// # Example
278///
279/// ```ignore
280/// // Protobuf message generated by prost (with serde support)
281/// #[derive(Clone, PartialEq, prost::Message, serde::Serialize)]
282/// pub struct CreateUserRequest {
283/// #[prost(string, tag = "1")]
284/// pub name: String,
285/// #[prost(string, tag = "2")]
286/// pub email: String,
287/// #[prost(int32, tag = "3")]
288/// pub age: i32,
289/// }
290///
291/// vld_tonic::impl_validate!(CreateUserRequest {
292/// name => vld::string().min(2).max(50),
293/// email => vld::string().email(),
294/// age => vld::number().int().min(0).max(150),
295/// });
296///
297/// // Now you can use:
298/// // msg.validate()?; — inherent method
299/// // msg.is_valid() — inherent method
300/// // vld_tonic::validate(req) — request helper
301/// ```
302#[macro_export]
303macro_rules! impl_validate {
304 ($name:ident { $($field:ident => $schema:expr),* $(,)? }) => {
305 ::vld::impl_rules!($name { $($field => $schema),* });
306
307 impl $crate::VldMessage for $name {
308 fn vld_validate(&self) -> ::std::result::Result<(), ::vld::error::VldError> {
309 self.validate()
310 }
311 }
312 };
313}
314
315// ========================= Helpers ===========================================
316
317fn coerce_value(s: &str) -> serde_json::Value {
318 if s.is_empty() {
319 return serde_json::Value::Null;
320 }
321 if s == "true" {
322 return serde_json::Value::Bool(true);
323 }
324 if s == "false" {
325 return serde_json::Value::Bool(false);
326 }
327 if let Ok(n) = s.parse::<i64>() {
328 return serde_json::Value::Number(n.into());
329 }
330 if let Ok(n) = s.parse::<f64>() {
331 if let Some(num) = serde_json::Number::from_f64(n) {
332 return serde_json::Value::Number(num);
333 }
334 }
335 serde_json::Value::String(s.to_string())
336}
337
338/// Prelude — import everything you need.
339pub mod prelude {
340 pub use crate::{
341 impl_validate, metadata_interceptor, validate, validate_metadata, validate_ref,
342 validate_with, validate_with_ref, validated_metadata, vld_status, vld_status_with_code,
343 VldMessage,
344 };
345 pub use vld::prelude::*;
346}