anycms-core 0.5.4

A unified API response library supporting multiple Rust web frameworks
Documentation
//! Integration with the [`validator`](https://docs.rs/validator) crate.
//!
//! Enabled by the `validator` feature flag.
//!
//! Provides automatic conversion from `validator::ValidationErrors`
//! to [`FieldError`] and [`ApiResult`].
//!
//! # Basic Usage
//!
//! ```rust,ignore
//! use anycms_core::ApiResult;
//! use validator::Validate;
//!
//! #[derive(Validate)]
//! struct SignupData {
//!     #[validate(email)]
//!     email: String,
//!     #[validate(length(min = 1))]
//!     name: String,
//! }
//!
//! fn handle_signup(data: SignupData) -> ApiResult<String> {
//!     if let Err(errors) = data.validate() {
//!         return errors.into();
//!     }
//!     ApiResult::value("ok".to_string())
//! }
//! ```

#[cfg(feature = "validator")]
use crate::result::{ApiResult, FieldError};
#[cfg(feature = "validator")]
use validator::{ValidationErrors, ValidationErrorsKind};

/// Convert `validator::ValidationErrors` into a flat list of [`FieldError`].
///
/// Handles nested struct and list errors by prefixing field names
/// (e.g., `"address.city"`, `"items.0.name"`).
///
/// # Example
///
/// ```rust,ignore
/// use anycms_core::validation_errors_to_field_errors;
/// use validator::Validate;
///
/// #[derive(Validate)]
/// struct Form {
///     #[validate(email)]
///     email: String,
/// }
///
/// let form = Form { email: "bad".into() };
/// if let Err(errors) = form.validate() {
///     let field_errors = validation_errors_to_field_errors(&errors);
///     for fe in &field_errors {
///         println!("{}: {}", fe.field, fe.message);
///     }
/// }
/// ```
#[cfg(feature = "validator")]
pub fn validation_errors_to_field_errors(errors: &ValidationErrors) -> Vec<FieldError> {
    let mut result = Vec::new();
    collect_field_errors(errors, "", &mut result);
    result
}

#[cfg(feature = "validator")]
fn collect_field_errors(errors: &ValidationErrors, parent_prefix: &str, acc: &mut Vec<FieldError>) {
    for (field_name, kind) in errors.errors() {
        let prefixed_name = if parent_prefix.is_empty() {
            field_name.to_string()
        } else {
            format!("{}.{}", parent_prefix, field_name)
        };

        match kind {
            ValidationErrorsKind::Field(vec) => {
                for validation_error in vec {
                    let message = format_validation_error_message(validation_error);
                    acc.push(FieldError::new(&prefixed_name, message));
                }
            }
            ValidationErrorsKind::Struct(nested) => {
                collect_field_errors(nested, &prefixed_name, acc);
            }
            ValidationErrorsKind::List(map) => {
                for (index, nested) in map {
                    let list_prefix = format!("{}.{}", prefixed_name, index);
                    collect_field_errors(nested, &list_prefix, acc);
                }
            }
        }
    }
}

/// Format a human-readable message from a `ValidationError`.
///
/// Uses the error's `message` field if present, otherwise falls back
/// to the `Display` implementation.
#[cfg(feature = "validator")]
fn format_validation_error_message(err: &validator::ValidationError) -> String {
    match &err.message {
        Some(msg) => msg.to_string(),
        None => err.to_string(),
    }
}

/// Convert `ValidationErrors` directly into a failed `ApiResult<T>`.
///
/// Produces a validation error response (422) with field-level error details.
///
/// # Example
///
/// ```rust,ignore
/// fn handler(data: SignupData) -> ApiResult<String> {
///     if let Err(errors) = data.validate() {
///         return errors.into(); // Auto-converts to 422 response
///     }
///     ApiResult::value("ok".to_string())
/// }
/// ```
#[cfg(feature = "validator")]
impl<T> From<ValidationErrors> for ApiResult<T> {
    fn from(errors: ValidationErrors) -> Self {
        let field_errors = validation_errors_to_field_errors(&errors);
        ApiResult::validation_errors(field_errors)
    }
}