Skip to main content

sova_vld/
error.rs

1use sova_core::extend::ErrorResponse;
2use sova_core::{Error, IntoResponse, Request, Response};
3use serde::Serialize;
4use vld::error::{IssueCode, PathSegment, ValidationIssue, VldError};
5
6/// Newtype so Sova can implement [`IntoResponse`] (orphan rule).
7#[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    /// Build a response, optionally using Accept / flash (feature `flash`).
52    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 {}