tinc/private/
validation.rs

1use axum::response::IntoResponse;
2
3use super::{
4    HttpErrorResponse, HttpErrorResponseCode, HttpErrorResponseDetails, HttpErrorResponseRequestViolation, TrackerFor,
5    TrackerSharedState, TrackerWrapper,
6};
7
8#[derive(Debug, thiserror::Error)]
9pub enum ValidationError {
10    #[error("error evaluating expression `{expression}` on field `{field}`: {error}")]
11    Expression {
12        field: &'static str,
13        error: Box<str>,
14        expression: &'static str,
15    },
16    #[error("{0}")]
17    FailFast(Box<str>),
18}
19
20impl serde::de::Error for ValidationError {
21    fn custom<T>(msg: T) -> Self
22    where
23        T: std::fmt::Display,
24    {
25        Self::FailFast(msg.to_string().into_boxed_str())
26    }
27}
28
29#[cfg(feature = "tonic")]
30impl From<ValidationError> for tonic::Status {
31    fn from(value: ValidationError) -> Self {
32        tonic::Status::internal(value.to_string())
33    }
34}
35
36impl IntoResponse for ValidationError {
37    fn into_response(self) -> axum::response::Response {
38        let message = self.to_string();
39        HttpErrorResponse {
40            code: HttpErrorResponseCode::Internal,
41            message: &message,
42            details: HttpErrorResponseDetails::default(),
43        }
44        .into_response()
45    }
46}
47
48impl From<ValidationError> for axum::response::Response {
49    fn from(value: ValidationError) -> Self {
50        value.into_response()
51    }
52}
53
54pub trait TincValidate
55where
56    Self: TrackerFor,
57    Self::Tracker: TrackerWrapper,
58{
59    fn validate(&self, tracker: Option<&Self::Tracker>) -> Result<(), ValidationError>;
60
61    #[allow(clippy::result_large_err)]
62    fn validate_http(&self, mut state: TrackerSharedState, tracker: &Self::Tracker) -> Result<(), axum::response::Response> {
63        tinc_cel::CelMode::Serde.set();
64
65        state.in_scope(|| self.validate(Some(tracker)))?;
66
67        if state.errors.is_empty() {
68            Ok(())
69        } else {
70            let mut details = HttpErrorResponseDetails::default();
71
72            for error in &state.errors {
73                details.request.violations.push(HttpErrorResponseRequestViolation {
74                    field: error.path.as_ref(),
75                    description: error.message(),
76                })
77            }
78
79            Err(HttpErrorResponse {
80                code: HttpErrorResponseCode::InvalidArgument,
81                message: "bad request",
82                details,
83            }
84            .into_response())
85        }
86    }
87
88    #[cfg(feature = "tonic")]
89    #[allow(clippy::result_large_err)]
90    fn validate_tonic(&self) -> Result<(), tonic::Status> {
91        tinc_cel::CelMode::Proto.set();
92
93        use tonic_types::{ErrorDetails, StatusExt};
94
95        use crate::__private::TrackerSharedState;
96
97        let mut state = TrackerSharedState::default();
98
99        state.in_scope(|| self.validate(None))?;
100
101        if !state.errors.is_empty() {
102            let mut details = ErrorDetails::new();
103
104            for error in state.errors {
105                details.add_bad_request_violation(error.path.as_ref(), error.message());
106            }
107
108            Err(tonic::Status::with_error_details(
109                tonic::Code::InvalidArgument,
110                "bad request",
111                details,
112            ))
113        } else {
114            Ok(())
115        }
116    }
117}
118
119impl<V> TincValidate for Box<V>
120where
121    V: TincValidate,
122    V::Tracker: TrackerWrapper,
123{
124    fn validate(&self, tracker: Option<&Self::Tracker>) -> Result<(), ValidationError> {
125        self.as_ref().validate(tracker.map(|t| t.as_ref()))
126    }
127}