use http::StatusCode;
use jsonapi_core::ApiError;
use jsonapi_http::{ApiErrorExt, with_status};
use validator::ValidationErrors;
use crate::JsonApiError;
impl JsonApiError {
#[must_use]
pub fn from_validation_errors(errors: &ValidationErrors) -> Self {
Self::from_api_errors(from_validation_errors(errors))
}
}
#[must_use]
pub fn from_validation_errors(errors: &ValidationErrors) -> Vec<ApiError> {
let mut fields: Vec<_> = errors.field_errors().into_iter().collect();
fields.sort_by(|(a, _), (b, _)| a.cmp(b));
let mut api_errors = Vec::new();
for (field, violations) in fields {
for violation in violations {
let detail = violation
.message
.as_ref()
.map_or_else(|| violation.code.to_string(), |m| m.to_string());
api_errors.push(
with_status(StatusCode::UNPROCESSABLE_ENTITY)
.pointer(format!("/data/attributes/{field}"))
.code(violation.code.to_string())
.detail(detail),
);
}
}
api_errors
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
use validator::ValidationError;
fn field_error(code: &'static str, message: Option<&'static str>) -> ValidationError {
ValidationError {
code: Cow::Borrowed(code),
message: message.map(Cow::Borrowed),
params: std::collections::HashMap::new(),
}
}
#[test]
fn two_invalid_fields_map_to_two_422_errors_with_pointers() {
let mut errors = ValidationErrors::new();
errors.add("title", field_error("length", Some("must not be empty")));
errors.add("age", field_error("range", None));
let api = from_validation_errors(&errors);
assert_eq!(api.len(), 2);
assert_eq!(api[0].status.as_deref(), Some("422"));
assert_eq!(
api[0].source.as_ref().unwrap().pointer.as_deref(),
Some("/data/attributes/age")
);
assert_eq!(api[0].detail.as_deref(), Some("range")); assert_eq!(api[0].code.as_deref(), Some("range"));
assert_eq!(
api[1].source.as_ref().unwrap().pointer.as_deref(),
Some("/data/attributes/title")
);
assert_eq!(api[1].detail.as_deref(), Some("must not be empty"));
}
#[test]
fn json_api_error_from_validation_errors_aggregates_into_one_document() {
let mut errors = ValidationErrors::new();
errors.add("title", field_error("length", Some("must not be empty")));
errors.add("age", field_error("range", None));
use axum::response::IntoResponse;
let err = JsonApiError::from_validation_errors(&errors);
let response = err.into_response();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
}