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