tinc/private/
validation.rs1use 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}