fraiseql-core 2.10.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
//! Validation engine for custom GraphQL scalars.
//!
//! Provides utilities to validate custom scalar values in different contexts.

use serde_json::Value;

use super::custom_scalar::CustomScalar;
use crate::error::{FraiseQLError, Result};

/// Validation context for custom scalar operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationContext {
    /// Serialize a database value to GraphQL response.
    Serialize,

    /// Parse a variable value from GraphQL operation.
    ParseValue,

    /// Parse a literal value from GraphQL query string.
    ParseLiteral,
}

impl ValidationContext {
    /// Get the string representation of this context.
    #[must_use]
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::Serialize => "serialize",
            Self::ParseValue => "parseValue",
            Self::ParseLiteral => "parseLiteral",
        }
    }
}

/// Error returned when custom scalar validation fails.
#[derive(Debug, Clone)]
pub struct ScalarValidationError {
    /// Name of the scalar that failed validation.
    pub scalar_name: String,

    /// Context in which validation occurred.
    pub context: String,

    /// Underlying error message.
    pub message: String,
}

impl ScalarValidationError {
    /// Create a new scalar validation error.
    pub fn new(
        scalar_name: impl Into<String>,
        context: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self {
            scalar_name: scalar_name.into(),
            context:     context.into(),
            message:     message.into(),
        }
    }

    /// Convert to `FraiseQLError`.
    #[must_use]
    pub fn into_fraiseql_error(self) -> FraiseQLError {
        FraiseQLError::validation(self.to_string())
    }
}

impl std::fmt::Display for ScalarValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Scalar \"{}\" validation failed in {}: {}",
            self.scalar_name, self.context, self.message
        )
    }
}

impl std::error::Error for ScalarValidationError {}

/// Validate a custom scalar value in a given context.
///
/// # Arguments
///
/// * `scalar` - The custom scalar implementation
/// * `value` - The value to validate
/// * `context` - The validation context
///
/// # Errors
///
/// Returns `ScalarValidationError` if validation fails.
///
/// # Example
///
/// ```
/// use fraiseql_core::validation::{validate_custom_scalar, ValidationContext, CustomScalar};
/// use fraiseql_core::error::Result;
/// use serde_json::{Value, json};
///
/// #[derive(Debug)]
/// struct Email;
/// impl CustomScalar for Email {
///     fn name(&self) -> &str { "Email" }
///     fn serialize(&self, value: &Value) -> Result<Value> { Ok(value.clone()) }
///     fn parse_value(&self, value: &Value) -> Result<Value> {
///         let str_val = value.as_str().unwrap();
///         if !str_val.contains('@') {
///             return Err(fraiseql_core::error::FraiseQLError::validation("invalid email"));
///         }
///         Ok(value.clone())
///     }
///     fn parse_literal(&self, ast: &Value) -> Result<Value> {
///         self.parse_value(ast)
///     }
/// }
///
/// let email = Email;
/// let result = validate_custom_scalar(&email, &json!("test@example.com"), ValidationContext::ParseValue).unwrap();
/// assert_eq!(result, json!("test@example.com"));
/// ```
pub fn validate_custom_scalar(
    scalar: &dyn CustomScalar,
    value: &Value,
    context: ValidationContext,
) -> Result<Value> {
    match context {
        ValidationContext::Serialize => scalar.serialize(value).map_err(|e| {
            FraiseQLError::validation(format!(
                "Scalar \"{}\" validation failed in serialize: {}",
                scalar.name(),
                e
            ))
        }),

        ValidationContext::ParseValue => scalar.parse_value(value).map_err(|e| {
            FraiseQLError::validation(format!(
                "Scalar \"{}\" validation failed in parseValue: {}",
                scalar.name(),
                e
            ))
        }),

        ValidationContext::ParseLiteral => scalar.parse_literal(value).map_err(|e| {
            FraiseQLError::validation(format!(
                "Scalar \"{}\" validation failed in parseLiteral: {}",
                scalar.name(),
                e
            ))
        }),
    }
}

/// Convenience function that defaults context to `ParseValue`.
///
/// # Errors
///
/// Returns `FraiseQLError::Validation` if the value does not conform to the scalar's format.
pub fn validate_custom_scalar_parse_value(
    scalar: &dyn CustomScalar,
    value: &Value,
) -> Result<Value> {
    validate_custom_scalar(scalar, value, ValidationContext::ParseValue)
}