Skip to main content

fraiseql_core/validation/
scalar_validator.rs

1//! Validation engine for custom GraphQL scalars.
2//!
3//! Provides utilities to validate custom scalar values in different contexts.
4
5use serde_json::Value;
6
7use super::custom_scalar::CustomScalar;
8use crate::error::{FraiseQLError, Result};
9
10/// Validation context for custom scalar operations.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ValidationContext {
14    /// Serialize a database value to GraphQL response.
15    Serialize,
16
17    /// Parse a variable value from GraphQL operation.
18    ParseValue,
19
20    /// Parse a literal value from GraphQL query string.
21    ParseLiteral,
22}
23
24impl ValidationContext {
25    /// Get the string representation of this context.
26    #[must_use]
27    pub const fn as_str(&self) -> &'static str {
28        match self {
29            Self::Serialize => "serialize",
30            Self::ParseValue => "parseValue",
31            Self::ParseLiteral => "parseLiteral",
32        }
33    }
34}
35
36/// Error returned when custom scalar validation fails.
37#[derive(Debug, Clone)]
38pub struct ScalarValidationError {
39    /// Name of the scalar that failed validation.
40    pub scalar_name: String,
41
42    /// Context in which validation occurred.
43    pub context: String,
44
45    /// Underlying error message.
46    pub message: String,
47}
48
49impl ScalarValidationError {
50    /// Create a new scalar validation error.
51    pub fn new(
52        scalar_name: impl Into<String>,
53        context: impl Into<String>,
54        message: impl Into<String>,
55    ) -> Self {
56        Self {
57            scalar_name: scalar_name.into(),
58            context:     context.into(),
59            message:     message.into(),
60        }
61    }
62
63    /// Convert to `FraiseQLError`.
64    #[must_use]
65    pub fn into_fraiseql_error(self) -> FraiseQLError {
66        FraiseQLError::validation(self.to_string())
67    }
68}
69
70impl std::fmt::Display for ScalarValidationError {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(
73            f,
74            "Scalar \"{}\" validation failed in {}: {}",
75            self.scalar_name, self.context, self.message
76        )
77    }
78}
79
80impl std::error::Error for ScalarValidationError {}
81
82/// Validate a custom scalar value in a given context.
83///
84/// # Arguments
85///
86/// * `scalar` - The custom scalar implementation
87/// * `value` - The value to validate
88/// * `context` - The validation context
89///
90/// # Errors
91///
92/// Returns `ScalarValidationError` if validation fails.
93///
94/// # Example
95///
96/// ```
97/// use fraiseql_core::validation::{validate_custom_scalar, ValidationContext, CustomScalar};
98/// use fraiseql_core::error::Result;
99/// use serde_json::{Value, json};
100///
101/// #[derive(Debug)]
102/// struct Email;
103/// impl CustomScalar for Email {
104///     fn name(&self) -> &str { "Email" }
105///     fn serialize(&self, value: &Value) -> Result<Value> { Ok(value.clone()) }
106///     fn parse_value(&self, value: &Value) -> Result<Value> {
107///         let str_val = value.as_str().unwrap();
108///         if !str_val.contains('@') {
109///             return Err(fraiseql_core::error::FraiseQLError::validation("invalid email"));
110///         }
111///         Ok(value.clone())
112///     }
113///     fn parse_literal(&self, ast: &Value) -> Result<Value> {
114///         self.parse_value(ast)
115///     }
116/// }
117///
118/// let email = Email;
119/// let result = validate_custom_scalar(&email, &json!("test@example.com"), ValidationContext::ParseValue).unwrap();
120/// assert_eq!(result, json!("test@example.com"));
121/// ```
122pub fn validate_custom_scalar(
123    scalar: &dyn CustomScalar,
124    value: &Value,
125    context: ValidationContext,
126) -> Result<Value> {
127    match context {
128        ValidationContext::Serialize => scalar.serialize(value).map_err(|e| {
129            FraiseQLError::validation(format!(
130                "Scalar \"{}\" validation failed in serialize: {}",
131                scalar.name(),
132                e
133            ))
134        }),
135
136        ValidationContext::ParseValue => scalar.parse_value(value).map_err(|e| {
137            FraiseQLError::validation(format!(
138                "Scalar \"{}\" validation failed in parseValue: {}",
139                scalar.name(),
140                e
141            ))
142        }),
143
144        ValidationContext::ParseLiteral => scalar.parse_literal(value).map_err(|e| {
145            FraiseQLError::validation(format!(
146                "Scalar \"{}\" validation failed in parseLiteral: {}",
147                scalar.name(),
148                e
149            ))
150        }),
151    }
152}
153
154/// Convenience function that defaults context to `ParseValue`.
155///
156/// # Errors
157///
158/// Returns `FraiseQLError::Validation` if the value does not conform to the scalar's format.
159pub fn validate_custom_scalar_parse_value(
160    scalar: &dyn CustomScalar,
161    value: &Value,
162) -> Result<Value> {
163    validate_custom_scalar(scalar, value, ValidationContext::ParseValue)
164}