Skip to main content

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}