vld 0.5.0

Type-safe runtime validation library for Rust, inspired by Zod
Documentation
//! Results of lenient parsing (`validate_fields()` / `parse_lenient()`).

use crate::error::{format_value_short, VldError};

/// Result of validating a single field.
///
/// Returned by `validate_fields()` generated by the [`schema!`](crate::schema!) macro.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FieldResult {
    /// Field name.
    pub name: String,
    /// Raw input value (before validation).
    pub input: serde_json::Value,
    /// `Ok(json_value)` if the field is valid (output serialized to JSON),
    /// `Err(error)` if validation failed.
    pub result: Result<serde_json::Value, VldError>,
}

impl FieldResult {
    /// Whether this field passed validation.
    pub fn is_ok(&self) -> bool {
        self.result.is_ok()
    }

    /// Whether this field failed validation.
    pub fn is_err(&self) -> bool {
        self.result.is_err()
    }
}

impl std::fmt::Display for FieldResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.result {
            Ok(v) => write!(f, "✔ {}: {}", self.name, format_value_short(v)),
            Err(e) => {
                let received = format_value_short(&self.input);
                for (i, issue) in e.issues.iter().enumerate() {
                    if i > 0 {
                        writeln!(f)?;
                    }
                    write!(
                        f,
                        "✖ {}: {} (received: {})",
                        self.name, issue.message, received
                    )?;
                }
                Ok(())
            }
        }
    }
}

/// Result of lenient parsing: contains the (possibly partial) struct and
/// per-field diagnostics.
///
/// Created by `parse_lenient()` / `parse_lenient_value()` generated by
/// [`impl_validate_fields!`](crate::impl_validate_fields).
///
/// The struct is always constructed — invalid fields fall back to
/// `Default::default()`. You can inspect which fields passed/failed via
/// [`fields()`](Self::fields), and save the result to a file at any time
/// via `save_to_file()`.
#[derive(Debug)]
pub struct ParseResult<T> {
    /// The constructed struct (invalid fields use `Default`).
    pub value: T,
    /// Per-field validation diagnostics.
    field_results: Vec<FieldResult>,
}

impl<T> ParseResult<T> {
    /// Create a new parse result.
    pub fn new(value: T, field_results: Vec<FieldResult>) -> Self {
        Self {
            value,
            field_results,
        }
    }

    /// All per-field results.
    pub fn fields(&self) -> &[FieldResult] {
        &self.field_results
    }

    /// Get a specific field's result by name.
    ///
    /// Returns `None` if no field with that name exists.
    pub fn field(&self, name: &str) -> Option<&FieldResult> {
        self.field_results.iter().find(|f| f.name == name)
    }

    /// Only the fields that passed validation.
    pub fn valid_fields(&self) -> Vec<&FieldResult> {
        self.field_results.iter().filter(|f| f.is_ok()).collect()
    }

    /// Only the fields that failed validation.
    pub fn error_fields(&self) -> Vec<&FieldResult> {
        self.field_results.iter().filter(|f| f.is_err()).collect()
    }

    /// Whether all fields passed validation.
    pub fn is_valid(&self) -> bool {
        self.field_results.iter().all(|f| f.is_ok())
    }

    /// Whether at least one field failed.
    pub fn has_errors(&self) -> bool {
        self.field_results.iter().any(|f| f.is_err())
    }

    /// Number of valid fields.
    pub fn valid_count(&self) -> usize {
        self.field_results.iter().filter(|f| f.is_ok()).count()
    }

    /// Number of invalid fields.
    pub fn error_count(&self) -> usize {
        self.field_results.iter().filter(|f| f.is_err()).count()
    }

    /// Consume and return the inner struct.
    pub fn into_value(self) -> T {
        self.value
    }

    /// Consume and return both the struct and field results.
    pub fn into_parts(self) -> (T, Vec<FieldResult>) {
        (self.value, self.field_results)
    }
}

impl<T: serde::Serialize> ParseResult<T> {
    /// Serialize the struct to a JSON file.
    pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
        let json = serde_json::to_string_pretty(&self.value)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        if let Some(parent) = path.parent() {
            if !parent.as_os_str().is_empty() {
                std::fs::create_dir_all(parent)?;
            }
        }
        std::fs::write(path, json)
    }

    /// Serialize the struct to a JSON string.
    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(&self.value)
    }

    /// Serialize the struct to a `serde_json::Value`.
    pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
        serde_json::to_value(&self.value)
    }
}

impl<T: std::fmt::Debug> std::fmt::Display for ParseResult<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(
            f,
            "ParseResult ({} valid, {} errors):",
            self.valid_count(),
            self.error_count()
        )?;
        for field in &self.field_results {
            writeln!(f, "  {}", field)?;
        }
        Ok(())
    }
}