1use sova_core::extend::ErrorResponse;
2use sova_core::{Error, IntoResponse, Request, Response};
3use serde::Serialize;
4use vld::error::{IssueCode, PathSegment, ValidationIssue, VldError};
5
6#[derive(Debug, Clone)]
8pub struct ValidationError(pub VldError);
9
10impl From<VldError> for ValidationError {
11 fn from(err: VldError) -> Self {
12 Self(err)
13 }
14}
15
16impl From<Error> for ValidationError {
17 fn from(err: Error) -> Self {
18 match err {
19 Error::PayloadTooLarge => Self(VldError::single(
20 IssueCode::TooBig {
21 maximum: 0.0,
22 inclusive: true,
23 },
24 "Payload Too Large",
25 )),
26 Error::BadRequest(msg) => Self(VldError::single(IssueCode::ParseError, msg)),
27 other => Self(VldError::single(
28 IssueCode::Custom {
29 code: "request_error".into(),
30 },
31 other.to_string(),
32 )),
33 }
34 }
35}
36
37impl ValidationError {
38 pub fn status_code(&self) -> u16 {
39 if self.0.issues.iter().any(|i| {
40 matches!(i.code, IssueCode::TooBig { .. }) && i.message.contains("Payload Too Large")
41 }) {
42 return 413;
43 }
44 if self.0.issues.iter().any(is_client_syntax) {
45 400
46 } else {
47 422
48 }
49 }
50
51 pub fn respond(self, req: &Request) -> Response {
53 #[cfg(feature = "flash")]
54 {
55 if wants_html(req) {
56 return self.respond_flash(req);
57 }
58 }
59 let _ = req;
60 self.into_response()
61 }
62
63 #[cfg(feature = "flash")]
64 fn respond_flash(self, req: &Request) -> Response {
65 use sova_core::{FormData, Redirect};
66 use sova_session::SessionExt;
67 use serde_json::json;
68
69 let session = req.session();
70 let mut errors = serde_json::Map::new();
71 for issue in &self.0.issues {
72 let path = format_path(&issue.path);
73 let key = if path.is_empty() {
74 "_form".into()
75 } else {
76 path
77 };
78 errors.insert(key, json!(issue.message));
79 }
80 session.flash_errors(&serde_json::Value::Object(errors));
81
82 let mut old = serde_json::Map::new();
83 if let Some(data) = req.get::<FormData>() {
84 for (k, values) in data.text_map() {
85 match values.as_slice() {
86 [] => {}
87 [one] => {
88 old.insert(k.clone(), json!(one));
89 }
90 many => {
91 old.insert(
92 k.clone(),
93 json!(many.to_vec()),
94 );
95 }
96 }
97 }
98 } else {
99 for (k, v) in &req.query {
100 old.insert(k.clone(), json!(v));
101 }
102 for (k, v) in &req.params {
103 old.insert(k.clone(), json!(v));
104 }
105 }
106 session.flash_old(&serde_json::Value::Object(old));
107
108 Redirect::back(req).into_response()
109 }
110}
111
112#[cfg(feature = "flash")]
113fn wants_html(req: &Request) -> bool {
114 let accept = req.header("accept").unwrap_or("*/*");
115 if accept.contains("application/json") && !accept.contains("text/html") {
116 return false;
117 }
118 accept.contains("text/html")
119}
120
121fn is_client_syntax(issue: &ValidationIssue) -> bool {
122 match &issue.code {
123 IssueCode::ParseError => true,
124 IssueCode::InvalidType { .. } if issue.path.is_empty() => true,
125 _ => false,
126 }
127}
128
129pub(crate) fn format_path(path: &[PathSegment]) -> String {
130 let mut out = String::new();
131 for (i, seg) in path.iter().enumerate() {
132 if i > 0 {
133 out.push('.');
134 }
135 match seg {
136 PathSegment::Field(name) => out.push_str(name),
137 PathSegment::Index(idx) => out.push_str(&idx.to_string()),
138 }
139 }
140 out
141}
142
143pub(crate) fn issue_code_slug(code: &IssueCode) -> String {
144 match code {
145 IssueCode::InvalidType { .. } => "invalid_type".into(),
146 IssueCode::TooSmall { .. } => "too_small".into(),
147 IssueCode::TooBig { .. } => "too_big".into(),
148 IssueCode::InvalidString { validation } => format!("invalid_string_{validation:?}")
149 .to_ascii_lowercase()
150 .replace([' ', '{', '}'], ""),
151 IssueCode::NotInt => "not_int".into(),
152 IssueCode::NotFinite => "not_finite".into(),
153 IssueCode::MissingField => "missing_field".into(),
154 IssueCode::UnrecognizedField => "unrecognized_field".into(),
155 IssueCode::IoError => "io_error".into(),
156 IssueCode::ParseError => "parse_error".into(),
157 IssueCode::Custom { code } => code.clone(),
158 }
159}
160
161#[derive(Serialize)]
162struct IssueBody {
163 path: String,
164 code: String,
165 message: String,
166}
167
168impl IntoResponse for ValidationError {
169 fn into_response(self) -> Response {
170 use sova_core::problem_with_errors;
171 let status = self.status_code();
172 let errors: Vec<IssueBody> = self
173 .0
174 .issues
175 .into_iter()
176 .map(|i| IssueBody {
177 path: format_path(&i.path),
178 code: issue_code_slug(&i.code),
179 message: i.message,
180 })
181 .collect();
182 problem_with_errors(status, "Validation Failed", "validation_failed", &errors)
183 }
184}
185
186impl From<ValidationError> for Error {
187 fn from(err: ValidationError) -> Self {
188 Error::Response(Box::new(err.into_response()))
189 }
190}
191
192impl ErrorResponse for ValidationError {}